计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 计算学部

学 号 1190202126

班 级 1936602

学 生 李映泽

指 导 教 师 刘宏伟

计算机科学与技术学院

2021年6月

摘 要

Hellow World!

表面上是平平无奇的hello在进行表演,可是它所表演背后的舞台,进程管理,虚拟内存系统,每一个步骤背后的ISA支持,是幕后的英雄。而本文就致力于探究hello一生背后的秘密。

本文通过对一个简简单单的hello程序进行分析,围绕着其全生命流程,展开了分析,从预处理,到编译,汇编,链接成.o文件,再到被加载入内存,成为进程,从进程管理,存储管理,IO管理的角度,对这个程序进行了进一步的探讨。

通过对计算机系统的漫游,从最外面的文本文件,一步一步,到了最底层的硬件实现,和操作系统的配合,使得对计算机系统的理解,更加深入。

**关键词:**汇编;编译;链接;操作系统;虚拟内存;硬件IO

**
**

目录

-第1章 概述 - 5 -

1.1 Hello简介 - 5 -

1.2 环境与工具 - 5 -

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上 - 5 -

软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS
64位/优麒麟 64位 - 5 -

开发与调试工具:gcc,vim,edb,readelf,HexEdit - 5 -

1.3 中间结果 - 5 -

1.4 本章小结 - 5 -

。第2章 预处理 - 7 -

2.1 预处理的概念与作用 - 7 -

2.2在Ubuntu下预处理的命令 - 7 -

2.3 Hello的预处理结果解析 - 7 -

2.4 本章小结 - 9 -

第3章 编译 - 10 -

3.1 编译的概念与作用 - 10 -

3.2 在Ubuntu下编译的命令 - 10 -

3.3 Hello的编译结果解析 - 11 -

数据: - 11 -

赋值: - 13 -

类型转换: - 14 -

算术操作 - 14 -

关系操作: - 15 -

控制转移: - 15 -

指针/数组操作 - 16 -

函数操作: - 17 -

开头: - 18 -

3.4 本章小结 - 18 -

第4章 汇编 - 19 -

4.1 汇编的概念与作用 - 19 -

4.2 在Ubuntu下汇编的命令 - 19 -

4.3 可重定位目标elf格式 - 19 -

4.4 Hello.o的结果解析 - 21 -

4.5 本章小结 - 22 -

第5章 链接 - 23 -

5.1 链接的概念与作用 - 23 -

5.2 在Ubuntu下链接的命令 - 23 -

5.3 可执行目标文件hello的格式 - 24 -

5.4 hello的虚拟地址空间 - 25 -

5.5 链接的重定位过程分析 - 29 -

5.6 hello的执行流程 - 32 -

5.7 Hello的动态链接分析 - 33 -

5.8 本章小结 - 34 -

第6章 hello进程管理 - 35 -

6.1 进程的概念与作用 - 35 -

6.2 简述壳Shell-bash的作用与处理流程 - 35 -

6.3 Hello的fork进程创建过程 - 36 -

6.4 Hello的execve过程 - 37 -

6.5 Hello的进程执行 - 38 -

6.6 hello的异常与信号处理 - 39 -

6.7本章小结 - 44 -

第7章 hello的存储管理 - 45 -

7.1 hello的存储器地址空间 - 45 -

7.2 Intel逻辑地址到线性地址的变换-段式管理 - 46 -

7.3 Hello的线性地址到物理地址的变换-页式管理 - 48 -

7.4 TLB与四级页表支持下的VA到PA的变换 - 50 -

7.5 三级Cache支持下的物理内存访问 - 52 -

7.6 hello进程fork时的内存映射 - 53 -

7.7 hello进程execve时的内存映射 - 54 -

7.8 缺页故障与缺页中断处理 - 54 -

7.9动态存储分配管理 - 55 -

7.10本章小结 - 57 -

第8章 hello的IO管理 - 59 -

8.1 Linux的IO设备管理方法 - 59 -

8.2 简述Unix IO接口及其函数 - 59 -

8.3 printf的实现分析 - 60 -

8.4 getchar的实现分析 - 62 -

8.5本章小结 - 63 -

结论 - 63 -

附件 - 64 -

参考文献 - 65 -

第1章 概述

1.1 Hello简介

P2P: From Program to Process从程序到进程

Hello在一开时,仅仅是内存里面的一段程序代码.在Bash里面,OS进程管理通过fork了一个新Process,然后对子进程进行execve,载入装载hello,并分给它时间片,让它得以成为进程.

0 2 0: From Zero-0 to Zero-0从零到零

本来没有Hello,是程序员用手,打出了文本文件,hello.c.然后,通过预处理,编译,汇编,链接,得到了ELF格式hello文件.当我们在Bash里面执行hello的时候,hello被装载进去.最后,执行结束以后,shell的父进程负责回收hello,而内核来删除相关的数据结构.最后,什么也没有留下.

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位

开发与调试工具:gcc,vim,edb,readelf,HexEdit

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

1.4 本章小结

介绍了hello的p2p,o2o过程,对hello的一生,进行了大体的定性说明.

罗列了本次实验的基本信息.环境与工具,中间结果.

并对中间结果进行了列表说明.

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

**概念:**预处理器(cpp)根据以字符#开头的命令,修改原始C程序。

例如:#include<stdio.h>将系统头文件里面的内容,直接插入到程序文本里面。得到了另外一个以.i结尾的C语言程序

**作用:**扩展C语言程序设计的环境。插入用#include的环境。同时拓展#define定义的宏。

2.2在Ubuntu下预处理的命令

通过命令:

gcc -E hello.c -o hello.i

进行预处理

对比:

左边是hello.i右边是hello.c

可以发现,在原有的基础上,又增加了许多的系统头文件的内容

2.3 Hello的预处理结果解析

  1. 在原hello.c里面有,但hello.i里面没有的:

注释内容,在预处理时,已经把程序里面的注释给删了。

  1. 在原hello.c里面有,在hello.i里面也有的:

主程序部分。

对比:

Hello.i Hello.c
  1. 在原hello.c里面没有,但在hello.i里面有的:

通过#include插入的一系列运行库的位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAXrMasl-1624784252313)(media/9cbadafa0a6ba334cf333eed7fac3b95.png)]

一些定义的结构体

还有一些定义的变量

和一些外部函数的名字

2.4 本章小结

本章通过介绍预处理阶段的概念以及在C语言程序中的作用

并在Linus环境里面对hello.c程序进行了预处理

初步探究了预处理的执行情况。

并对预处理的结果进行了解析

第3章 编译

3.1 编译的概念与作用

**概念:**编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。每条语句都是以文本形式描述的低级机器指令

**作用:**为高级语言的不同编译器提供了通用的输出语言。例如:C编译器与Fortran编译器产生的输出文件用的是同样的,依赖于自己CPU的语言。

3.2 在Ubuntu下编译的命令

首先:于键盘里面,按下:

gcc -S hello.i -o hello.s

然后打开编译完成的文本文件

3.3 Hello的编译结果解析

数据:

通过观察C语言程序,发现其中存在如下变量

  1. sleepsecs

这个变量是全局变量,于main函数外部声明。

  1. argc/argv

这个变量,是局部的,而且是调用main函数里面进行传入的参数。在IA64里面,它存放在rdi里面。

对main函数的汇编代码进行分析。

Hello.s Hello.c


|


|

在汇编的代码中,是将这个数据一开始放于寄存器rdi里面,然后,调用main函数的。

然后再进入main后,又将其放入堆栈里面进行保存。

再在后面如果是要使用到rdi与rsi时,均是从堆栈里面调数据

  1. i

i作为hello.c里面的局部变量,在main里面的循环中出现。

观察汇编代码发现,i在堆栈里面出现,并作为每次循环都进行更新的变量。

这种风格的汇编代码,每次都需要将变量放到栈里面进行读写,性能上,并不占优势。于是我开了O2优化

开O2优化以后

观察:

发现在开启优化后,程序将i就保存与%ebx里面。而不是存于栈中,减小了内存的读取。

赋值:

观察C语言程序,赋值出现了两次。

首先是全局变量的赋值:

在汇编里面观察,发现

即赋初值的全局变量为在程序运行前便已经赋值了的.

然后是局部变量的赋值.

在这里,i被初始化为0

观察相应汇编代码里面的:

即,局部变量于运行时对变量进行赋初值.行为与程序里面保持一致

同时,看其对应的汇编代码:

是movl,即是对32位的数据进行移动,正好对应int的四字节的要求

类型转换:

  1. 全局变量sleepsecs的类型转换

在C代码里面时int /赋值时float/最后在汇编的结果时long

注意,它是int,不过却是赋值2.5.

我们看看汇编代码里面是如何处理的。

编译器将这个变量已经转变成2了

在用到sleepsecs的时候,

不难发现,这个转换是隐式转换。自动将2.5转换为了2,而且,类型由int转为了long

算术操作

  1. C语言里面的++

注意到循环里面的i++

于是在汇编里面寻找对应

即正好对应在里面的值进行加1

注意到i是int类型,所以是进行32位的加法,即位addl

关系操作:

在程序里面存在两处关系操作,第一处是if条件里面的!=3判断

第二处是在循环里面的边界的判定

对应到汇编里面,其对应的语句就是:


对应的cmpl正是32位的比较,

控制转移:

在C代码里面有if与while语句,下面对其展开分析

针对C里面的if语句:

C 汇编

这种汇编的转换于CSAPP课本P143页里面提及.

针对C里面的while语句:

C 汇编

这个for循环是采用先赋初值,然后采用Jump to middle 策略改变的循环结构

将汇编代码改写为等价含义的C语言代码:

C 由汇编转换后的C

指针/数组操作

程序对传入的argv数组进行了引用.

argv数组是一个指针数组,即数组里面装的是指针

程序引用了argv[1]与argv[2]

和字符串一起,作为printf的三个参数,藏在for循环里面

下面进行分析:

首先在汇编代码里面定位到printf引用的这三个参数

引用的流程如下:

将数组的基地址取出 => 对基地址进行计算偏移量 => 取出内存里面的相应值

即最后一步其实是以argv[2]里面的内容为地址去访问内存.等价于*(argv[2])

函数操作:

main函数里面一共调用了4个函数:printf/exit/sleep/getchar

下面依次进行分析:

注意:这里所有的函数后缀都有@PLT这是动态链接的内容,我们将在链接章节里面介绍这两个函数!

printf

引用了两次.第一次是仅仅输出字符串,第二次是含参数的格式化输出.

第一处的printf

观察汇编代码,不难发现,第一处的输出字符串是被替换为了puts@PLT

这里面隐藏着gcc编译器的默认优化,即,如果只是输出一组字符串+\n的话,会被默认优化为puts()指令

同时,看看传入的参数,不难发现,即这个rdi里面存的是.LC0,.LC0里面装的正好就是字符串.
“Usage: Hello 学号 姓名!\n”

有关更细致的分析,将在下一章里面继续说明

exit

main函数是将1传给rdi里面后就直接调用exit@PLT了

sleep函数:

也是将参数传给rdi以后直接调用

getchar

开头:

注意:

这里,已经在头部对这个.s文件进行了一定的说明。

.file: 说明文件名字为从hello.c编译而来

.text 代码段

.globl 全局变量:说明sleepsecs是全局变量

.data 数据段

.align 对齐方式,.align 4说明是按四个字节对齐

.type sleepsec,说明这个变量是对象类型

.size 大小,说明这个变量占用的大小为4字节

.long 说明sleepsec的类型是long

.section 说明sleepsecs的节在于只读数据段

.LC0 与.LC1说明这是两个字符串,最后是需要被连接器进行重定位的。

最后是对main函数的定义:

Main函数在.text段里面,main是全局符号,然后它的type是function即函数

本章小结

本章具体讨论编译.

对编译的概念与作用,Ubuntu下编译的命令,Hello的编译结果解析进行了分析说明

其中,对Hello的编译结果同C语言里面的数据/变量赋值(局部/全局)/类型转换/算术操作/关系操作/控制转移/指针数组操作/函数操作进行了细致的探讨

第4章 汇编

4.1 汇编的概念与作用

**概念:**将.s文件翻译成机器语言指令,并将这些指令打包成可重定位目标程序的格式.将结果保存到.o文件里面

**作用:**将给人看的汇编文本文件,翻译成给机器看的二进制代码.

4.2 在Ubuntu下汇编的命令

命令:

as hello.s -o hello.o

生成了hello.o文件

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

通过readelf -a命令进行查看:

ELF头

节头部表:

描述了每一节的名字/大小,类型/条目size,地址/标志/链接/信息,偏移量/对齐方式,

文件里面没有动态节/程序头/节组

重定位节

注意到这些重定位的有些是静态链接,有些是动态链接.如R_X86_64PC32的是静态链接内容/而PLT_32则为动态链接内容.

还有一个与重定位有关的: .rela.eh_frame

.rela.eh_frame

eh_frame即exception handle frame即异常处理框架

就是说,这是与重定位相关的一个异常处理单元

最后是符号表

它有18个条目

4.4 Hello.o的结果解析

以下格式自行编排,编辑时删除

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

对比示意:左方为.o文件反汇编的结果.右方为.s

注意到基本的逻辑并没有改变,但是发现

1 原来的十进制数已经被翻译成了16进制数.

2 原来引用的一些全局变量,已经由<符号>(%rip)变成了
0x0(%rip),原来的符号蕴含的重定位信息被放到了表里面.(右方的信息是Objdump自己给我们辅助生成的)

3. 原来引用的一些函数,也是由<符号>(%rip)变成了 0x0(%rip),
原来的符号蕴含的重定位信息被放到了表里面.(右方的信息是Objdump自己给我们辅助生成的)

控制跳转指令的跳转位置,从jmp符号直接被转换为jmp指令族,其后方的二进制数代表了相对位置

4.5 本章小结

本章讨论汇编

对汇编的概念与作用/在Ubuntu下汇编的命令/可重定位目标elf格式/Hello.o的结果解析进行了分析与阐述

并探究了hello.o文件的Objdump与之前原来汇编格式之间的差异

第5章 链接

5.1 链接的概念与作用

概念*:链接时将各种代码和数据片段收集并组合成为一个单一文件的过程.这个文件可以被加载到内存里面执行.*

**作用:**在软件开发里面扮演着重要角色,使分离编译成为可能.

5.2 在Ubuntu下链接的命令

第一种方法:

指令:
ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

第二种方法:直接使用gcc进行操作

指令:
gcc hello.o -o hello

5.3 可执行目标文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

使用readelf进行分析,输入:

readelf -a hello > hello_2_elf

进行对ELF文件进行查看:

如图, 其各段的基本信息,如起始地址,大小等信息等,已被罗列如上.

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

首先,打开edb并加载程序文件

下面,开始查看hello的虚拟空间各个段的信息.

我们将对照之前笔者画的图片,挨个进行分析.

这个,是X86-64的虚拟空间示意图

我们利用edb的内存区域选项进行对内存区域的查看

两图一项一项的进行对照

在Memregions里面最下面一行的权限是只读区域,而且是不能执行的

我们对其进行dump

发现这个就是ELF的头!

其内容,是和我们之前readelf读的是一致的

往上,是权限是r-x权限的内存区域,即这个区域是可以执行的。

正好与我们在EDB里面看到的指令装载的地址一致

在往上看,权限是只读的。我们对其进行分析:

对之进行dump后发现这个区域是只读数据域,保存了我们代码里面的如字符串之类的数据

继续往上,发现权限是可读可写,这个是运行时堆,运行时,由malloc进行管理这部分内存的分配

往上,内存地址发生了较大变化,一共有三段,Memory
Regions指示我们,这个区域是共享库的区域。

再最后往上,到了我们的用户栈

不难发现,这个用户栈的结构,和书中一致,即返回地址,以及其上面的环境变量

最上面的是内核区域,我们没有办法访问。

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

1)首先使用objdump -d -r hello指令,生成对hello的反汇编文件

2)然后如图,进行对比

不难发现,左图中的hello.o文件里面只有一个函数即main函数,而右方的对hello进行反汇编的结果里面有一整套的函数。比如:.init段的_init函数

含汇编代码的段增多

从原来的只有的.text段,扩增为了不仅有.text段,也有.init段、.plt段、.fini段等等。

.text段的内容增多了。

左右均是.text段的内容,不过左方的.text段只有main函数,右方的.text段不仅包含.main,更包含main函数的很多入口函数,如_start之类。

增加了外部的共享库函数

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

为了分析重定位,我们采用-no-pie指令对hello.c进行重新编译。

我们以原来在main函数中的第一条需要重定位的代码为例展开分析:

重定位PC相对引用

它在elf文件里面的信息是:

这个条目r的类型是R_X86_64_PC32的类型

条目信息:
r.offset = 0x1c r.symbol = .rodata r.type = R_X86_64_PC32 r.addend = -4

下面开始对重定位的refptr进行计算:

通过链接器的计算,其已经确定如下信息:

在hello里面其.rodata的地址为:0x402000

 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210627172643714.png)

而main函数的地址是:0x4011b6

需要引用的字符串的地址Addr(r.symbol)=0x402004

调用CSAPP第三版第480页的重定位算法,进行计算:

计算过程:
refaddr =Addr(s)+r.offset =0x4011b6+0x1c =0x4011d2 *refptr = (unsigned)(Addr(r.symbol)+r.addend-refaddr) = (unsigned)(0x402004 +(-4) -0x4011d2) = 0xe2e

这个重定位计算得来的信息,正好与汇编代码里面的一致!

在得到的可执行目标文件里面,lea指令有着如下的形式:

以上,便是链接器对代码里面的条目进行重定位的过程

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call
main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

打开edb载入hello

**


**

列出所有过程:

输入:

./hello 1190202126 李映泽

子程序名 程序地址(16进制)
ld -2.27.so_dl_start 7efb ff4d8ea0
ld-2.27.so_dl_init 7efb ff4e7630
hello_start 400500
libc-2.27.so__libc_start_main 7efb ff100ab0
Hello_printf@plt(调用了10次) 4004c0
Hello_sleep@plt(调用了10次) 4004f0
hello!getchar@plt 4004d0
libc-2.27.so!exit 7efbff122120

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

关于PLT与GOT表的介绍:
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT
中存放函数目标地址,PLT使用 GO T中地址跳转到目标函数。

dl_init前后变化

在dl_init之前

利用gdb来进行打印内存地址内容:

在dl_init之后,

发现内容已经不同比如说

puts@got.plt 函数的地址,已经在dl_init之后,被改写了

进入这个地址

0x7ffff7e475a0

发现,是_GI_IO_PUTS函数

5.8 本章小结

本章讨论链接

对 链接的概念与作用 在Ubuntu下链接的命令 可执行目标文件hello的格式
hello的虚拟地址空间 链接的重定位过程/执行流程展开了分析

根据X86-64的虚拟地址空间模型,以hello为例,一一进行了对照比较分析.

同时,根据CSAPP书中所述的重定位算法,对链接的重定位过程进行了分析

并使用gdb作为工具,对hello的执行流程/动态链接过程进行了分析

第6章 hello进程管理

6.1 进程的概念与作用

**概念:**进程是一个执行中程序的实例.

进程定义:

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is
being executed)。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元

进程作用:

(以下内容节选自CSAPP P508)

1)进程为用户提供了以下假象:

我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行
我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

2)方便shell程序的构造

每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell
就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

3)两个关键抽象

进程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间。

6.2 简述壳Shell-bash的作用与处理流程

以下格式自行编排,编辑时删除

Shell的概念:

shell是一个交互型应用级程序,代表用户运行其他程序。是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。

Shell的功能:

代表用户运行其他程序.
接收用户输入的命令并把它送入内核去执行。.实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。

处理流程:

shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。

6.3 Hello的fork进程创建过程

运行中的进程可以通过fork()函数创建子进程。

fork() 函数简介:
#include <sys/types.h> #include <unistd.h> pid_t fork(void); //返回:子进程返回0;父进程返回子进程的PID 如果出错,返回-1

对于我们打开的shell而言:

首先读入我们敲入的命令行

将串”./hello 1190202126 李映泽”读入,作为一组参数。

对串进行解析

串被分割为**./hello1190202126李映泽**三个子串

读取第一个参数:

发现**./hello并不是内置参数,于是fork一个子进程,子进程去execve
hello命令,带上参数:”
./hello 1190202126 李映泽**”

进程图:

6.4 Hello的execve过程

Shell产生的子进程利用的是execve函数进行对新程序的加载与运行。

execve函数在当前调用的进程的上下文里面加载并运行一个程序。

exceve函数的说明
#include <unistd.h> int execve(const char * filename,const char *argv[] const char *envp[]); //如果加载成功,那么不返回,如果错误,返回-1 //这个函数加载并运行可执行目标文件filename //且带参数列表argv与环境变量envp //只有出现错误,才返回-1; //否则不会返回

在加载了hello以后,通过调用启动代码,设置完成启动栈的结构,并转移控制权给main函数

如图:新程序开始后,用户栈的典型结构如下:

6.5 Hello的进程执行

以下格式自行编排,编辑时删除

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

**进程时间片:**分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。或者说,一个进程执行它的控制流的一部分的每一时间段。

**进程上下文信息:**进程执行活动全过程的静态描述。为内核重新启动一个被抢占的进程所需要的状态。由如:通用目的寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈以及各类内核数据结构组成。

**调度:**在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)

*进程调度的过程:*上下文切换

  1. 保存当前进程的上下文

  2. 恢复某个之前被抢占的进程的上下文

  3. 将控制转移给这个新恢复的进程

用户态与核心态的转换:

即用户模式切换到内核模式

Hello一开始是运行在用户模式里面,在调用sleep、exit函数时,通过陷阱异常,实现syscall,将控制权给内核开始系统调用。在执行结束以后,控制权转移给main的用户模式。

6.6 hello的异常与信号处理

以下格式自行编排,编辑时删除

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

hello执行中出现的异常:

中断*:接收到来自处理器外部的I/O设备的信号.*

比如:我们在键盘里面敲入Ctrl
C/Z.,使得在处理器执行时,中断引脚的电压变高了…在处理完当前指令后,控制转移给处理程序;中断处理程序进行执行.执行结束后,返回到下一条指令.

陷阱*:有意的异常.例如为了进行系统调用.*

在hello的执行过程中,需要调用系统函数来进行一系列的操作:如printf进行的显示屏输出/sleep的睡眠/exit进行退出等等.这些系统调用,均要进行syscall,即陷阱,从用户模式转内核模式

hello执行中处理的信号:

SIGINT,SIGSTP,SIGCONT,SIGWINCH

信号处理演示:

Ctrl-Z

Ctrl-C(中断)


| 在ctrl-z以后的ps |
| **
** |
| 按下fg,在前台进行 |
|

|
| kill -9 PID(杀死进程) |
|

|
| 杀死后 ps |
|
|
| pstree |
|


将输出重定向至文本文件里面


|
| kill -19与kill -18的组合 |
| 首先将进程进行挂起:

-19 sigstop 将进程进行停止


-18 SIGCONT 将进程继续


|

说明:

按下键盘的时候:

中断异常示例:

引发了中断异常.如图:

而当程序在进行显示屏输出的时候:

引发的是陷阱:

通过陷阱,调用系统函数.将字符串打印到屏幕上面

信号处理:

对于kill -9 8810:

直接将SIGKILL发送到8810进程,使其直接终止.

对于CTRL C/Z

将SIGINT/SIGSTP由内核发送到8810号进程.SIGINT直接终止8810.而SIGSTP将8810号进程进行停止与挂起.

6.7本章小结

本章讨论hello的进程管理

对进程的概念与作用 壳Shell-bash的作用与处理流程
Hello的fork进程创建过程/execve过程/的进程执行/异常与信号处理

进行深入的探讨,并以图示的方式展开了说明

第7章 hello的存储管理

7.1 hello的存储器地址空间

以下格式自行编排,编辑时删除

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址: 段内偏移地址.
程序产生的和段相关的偏移地址部分.也就是hello在“段基址+段内偏移地址”访问模式下的段内偏移地址.

线性地址:
是逻辑地址到物理地址变换之间的中间层。逻辑地址是段里面的偏移地址,加上生成的段的基地址就是线性地址.
在执行hello时,我们位于保护模式下,于是我们的段基址寄存器通过段选择子在GDT里面找到真正的段基址,加上段内偏移地址,作为一个整体,这个地址叫做线性地址.

**虚拟地址:**如果开启分页,那么线性地址将作为虚拟地址,给CPU,通过查找页表,找到对应的物理地址.以hello为例,由于我们是在保护模式下,我们的虚拟地址就是和线性地址一样,其示例正如下图所示:

**物理地址:**是内存单元的绝对地址,也是存储管理的终点.由CPU发出的地址最终会被转化为物理地址.结合hello为例:就是最后hello在内存里面存的地址

7.2 Intel逻辑地址到线性地址的变换-段式管理

以下格式自行编排,编辑时删除

段式管理:
把一个程序分成若干个段(segment)进行存储,每个段都是一个逻辑实体(logical
entity),程序员需要知道并使用它。它的产生是与程序的模块化直接有关的。段式管理是通过段表进行的,它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。

通俗的说:这个变换,就是将逻辑地址,映射到线性地址.

**逻辑地址=**段标识符+段内偏移量

段内偏移量,是保持不变的.直接送到最后的加法器,用于合成线性地址

段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:

索引号,是在**”段描述符表”**的索引.

段描述表,可以细分为全局段描述符表(GDT)与局部段描述符表(LDT)

这两者的寻找的切换,是由上图的TI即表指示器进行决定的.

(Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。)

通过在段描述符表里面进行索引寻找,它可以寻找到段描述符(segment descriptor)

这个段描述符描述了一个段,由8个字节组成,如下图:

我们只关心BASE段,即描述了一个段的开始位置的线性地址.它与之前逻辑地址里面的offset偏移量加加在一起,就成了线性地址.

故大体流程如下:

段式管理
对逻辑地址进行分割成:段选择符+Offset段选择符的TI,如果是0,那么切换到GDT,如果是1,那么切换到LDT 利用段选择符里面的索引号在相应的表里面进行取出段描述符 对段描述符里面的Base进行取出 线性地址<=Base+Offset

7.3 Hello的线性地址到物理地址的变换-页式管理

由于我们开启了分页,所以,Hello将进行由线性地址->物理地址的变换

首先引入页的概念:

页:将N个连续字节的数组,成为页

页可分为虚拟页(PP)与物理页(PP)

如果不考虑多级页表等更加精细的机制,我们的地址映射将是这样进行的.

线性地址,即虚拟地址.

被分成了两部分

线性地址=VPN(虚拟页号)+VPO(虚拟页偏移量)

虚拟页号是一个索引,在当前进程的CR3寄存器指向当前的页表里面寻找虚拟页,并把里面存的物理页号PPN返回与物理页偏移量PPO一道,进行返回.

物理地址=PPN(物理页号)+PPO(物理页偏移量)

7.4 TLB与四级页表支持下的VA到PA的变换

以下格式自行编排,编辑时删除

TLB支持下的VA到PA的变换

目的:利用局部性原理,加快地址翻译速度.

TLB: 翻译后备缓冲器 (Translation Lookaside Buffer ,TLB)为了加速地址翻译

它就是一个Cache

具有以下特性:

▪ MMU中一个小的具有高相联度的集合

▪ 实现虚拟页号向物理页号的映射

▪ 页数很少的页表可以完全放在TLB中

如果缓存了的话,那么TLB可以直接将VPN映射到PTE,以上步骤都是在CPU内部的MMU单元完成的,极快!

四级页表支持下的VA到PA的变换

目的:利用多级页表,降低内存占用.

如果我们的页面大小4KB,48位地址空间,8字节的PTE,那么,我们的页表占用的空间至少应该是:

2^48 * 8 / 4KB = 2^(48+3-12)=2^39Byte

即我们需要512G的页表,这显然不现实,我们发现,其实有很多页,我们并没有用上,于是,可以直接索性不放入PTE里面,采用多级页表的处理思路.

以4级页表为例:

**大致流程:**由CR3寄存器指向L1的PT,然后由VPN1作为索引,进行寻找,找到PTE以后,以PTE条目里面的Base作为基址,再以VPN2作为索引,重复上述操作,直到找到L4的PT里面的PTE,以这个作为PPN,并上PPO,作为虚拟地址.

Core i7 1-3级页表条目格式
每个条目引用一个 4KB子页表: P: 子页表在物理内存中 (1)不在 (0) R/W: 对于所有可访问页,只读或者读写访问权限 U/S: 对于所有可访问页,用户user或超级用户supervisor(内核)模式访问权 限 WT: 子页表的直写或写回缓存策略 A: 引用位 (由MMU 在写时设置,由软件清除) PS: 页大小为4 KB 或 4 MB (只对第一层PTE定义) 页表物理基地址: 子页表物理基地址的最高40位 (强制物理页表 4KB 对齐) XD: 能/不能从这个PTE可访问的所有页中取指令
Core i7 第 4 级页表条目格式
每个条目引用一个4KB的页: P: 子页表在物理内存中 (1)不在 (0) R/W: 对于所有可访问页,只读或者读写访问权限 U/S: 对于所有可访问页,用户或超级用户 (内核)模式访问权限 WT: 子页表的直写或写回缓存策略 SD:能/不能缓存(Cache disabled or enabled) A: 引用位 (由MMU 在读或写时设置,由软件清除) D: 修改位(Dirty bit, 由MMU 在写时设置,由软件清除) 页表物理基地址: 物理页基地址的最高40位(强制物理页 4KB 对齐) XD: 能/不能从这个PTE可访问的所有页中取指令

7.5 三级Cache支持下的物理内存访问

以Corei7为例

对于给定的物理地址PA,首先在L1里面进行寻找.

(1)将PA分割成CT CI CO

CI是组号,定位了应该出现的组,CO是offset定位了偏移量.CT是tag即标识的tag.

(2)对本层的Cache进行寻找

硬件会定位到CI的组(组索引),然后对组里面的每一行,(L1里面有8行)进行比较tag,如果tag相同且有效,那么找到了

(3)如果找到,那么直接返回数据

(4)如果找不到,那么就会到L2里面进行寻找,重复上述操作直到找到为止.

(5)更新时,如果存在空闲的(有效0),那么直接替换,如果没有,根据LRU策略,找一个块进行驱逐.

7.6 hello进程fork时的内存映射

以下格式自行编排,

为新进程创建虚拟内存

▪ 创建当前进程的mm_struct 、vm_area_struct和页表的原样副本。

▪ 两个进程中的每个页面都标记为只读

▪ 两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)

在新进程中返回时,新进程拥有与调用fork的父进程相同的虚拟内存

随后的写操作通过写时复制机制创建新页面

7.7 hello进程execve时的内存映射

execve函数进行了以下步骤:

删除已存在的用户区域

删除当前进程虚拟地址的用户部分中已存在的内存区域

映射私有区域

将目标文件hello的代码和初始化的数据映射到.text和.data区

同时,将.bss和栈映射到匿名文件

映射共享区域:

将libc.so等内容映射到共享库里面的映射区域.

设置PC,指向代码区域的入口点

7.8 缺页故障与缺页中断处理

1) 处理器生成一个虚拟地址,并将其传送给MMU

2) MMU生成PTE地址(PTEA),并从高速缓存/主存请求得到PTE

3) 高速缓存/主存向MMU返回PTE

4) PTE的有效位为零, 因此 MMU 触发缺页异常

5) 缺页处理程序确定物理内存中的牺牲页 (若页面被修改,则换出到磁盘——写回策略)

6) 缺页处理程序调入新的页面,并更新内存中的PTE

7) 缺页处理程序返回到原来进程,再次执行导致缺页的指令

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

**动态内存管理:**需要维护着进程的一个虚拟内存区域,称为堆.

我们这里的管理,用到的时malloc,这个是显示分配器,即要求应用显式地释放任何已分配的块

约束条件:

▪ 可以处理任意的分配( malloc)和释放(free)请求序列

▪ 只能释放已分配的块,无法控制分配块的数量或大小

▪ 立即响应 malloc 请求

▪ 不允许分配器重新排列或者缓冲请求

▪ 必须从空闲内存中分配块

▪ 必须对齐块,使它们可以保存任何类型的数据对象

▪ 在 Linux 上:8字节 (x86) or 16字节 (x86-64) 对齐

▪ 只能操作或改变空闲块

▪ 一旦块被分配,就不允许修改或移动它了

目标: 最大化吞吐量,最大化内存利用率

基本方法与策略:

  1. 隐式空闲链表 (Implicit list)

通过头部中的大小字段隐含地连接空闲块

通过这些形式已经组织好了的块,进行连接在一起,维护了空闲块的结构

阴影:已分配块

空白:空闲块

头:大小/分配位

  1. 显式空闲链表 (Explicit list)

在空闲块中使用指针连接空闲块

实例:

  1. 分离的空闲列表 (Segregated free list)

空闲块按尺寸size分类/组,每个类/组使用一个空闲链表。

当分配器需要一个大小为n的块时:

▪ 搜索相应的空闲链表,其大小要满足m > n

▪ 如果找到了合适的块:

▪ 拆分块,并将剩余部分插入到适当的可选列表中

▪ 如果找不到合适的块, 就搜索下一个更大的大小类的空闲

链表

▪ 直到找到为止。

如果空闲链表中没有合适的块:

▪ 向操作系统请求额外的堆内存 (使用sbrk())

▪ 从这个新的堆内存中分配出 n 字节

▪ 将剩余部分放置在适当的大小类中

7.10本章小结

本章讨论hello的存储管理.

讨论了由硬件与操作系统支持的线性地址空间/虚拟地址空间/物理地址空间/逻辑地址空间之间的转化.
对hello的存储器地址空间,进行了分析.

基于Intel Corei7,
对/Hello的线性地址到物理地址的变换-页式管理/Intel逻辑地址到线性地址的变换-段式管理/TLB与四级页表支持下的VA到PA的变换/三级Cache支持下的物理内存访问进行了说明

同时,基于虚拟内存的概念,对hello进程fork时的内存映射/进程execve时的内存映射/缺页故障与缺页中断处理/动态存储分配管理等内容进行了分析说明

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

以下格式自行编排,编辑时删除

设备的模型化:文件

一个 Linux 文件 就是一个 m 字节的序列:

B 0 , B 1 , … , B k , … , B m-1

设备管理:unix io接口

这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix
I/O:

8.2 简述Unix IO接口及其函数

以下格式自行编排,编辑时删除

接口描述如下:

打开文件:应用程序请求内核打开相应的文件,宣告其想要访问IO设备

对应的函数:

改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k。

应用程序执行seek操作,显示设置文件的当前位置为k。

读写文件:一个读操作是从文件里面复制n >
0个字节到内存里面。从当前文件位置k开始,增加k到k+n。给定大小m文件,k>=m时,触发EOF条件,然后应用程序检测到。类似,写操作是从内存里面复制n>0字节到文件里面,从当前文件位置k开始,然后更新k。

关闭文件:应用程序完成了文件访问以后,通知内核来关闭文件。

打开文件的函数:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(char *filename,int flags,mode_t mode);
关闭文件的函数
#include <unistd.h> int close(int fd);
读写文件函数
#include <unistd.h> //读文件函数 ssize_t read(int fd,void* buf,size_t n); //写文件函数 ssize_t write(int fd,const void *buf,size_t n);

8.3 printf的实现分析

以下格式自行编排,编辑时删除

https://www.cnblogs.com/pianist/p/3315801.html

首先观察printf的函数体:

printf函数体:
int printf(const char *fmt, …) { int i; char buf[256]; va_list arg = (va_list)((char *)(&fmt) + 4); i = vsprintf(buf, fmt, arg); write(buf, i); return i; }

其中,调用了函数vsprrintf与write

vsprintf生成显示信息:

vsprintf
int vsprintf(char *buf, const char *fmt, va_list args) { char *p; char tmp[256]; va_list p_next_arg = args; for (p = buf; *fmt; fmt++) { if (*fmt != ‘%’) { *p++ = *fmt; continue; } fmt++;//将fmt字符串拷贝到buf缓冲里面,直到出现格式化串 switch (*fmt)//出现格式化串,针对不同的串,进行相应的操作 { case ‘x’: itoa(tmp, *((int *)p_next_arg)); strcpy(p, tmp); p_next_arg += 4; p += strlen(tmp); break; case ‘s’: break; default: break; } } return (p - buf); }

经过分析可知:

vsprintf是一个系统函数

vsprintf返回的是一个要打印的字符串的长度

通过接受确定输出格式的格式化字符串,对参数进行格式化,输出到格式化buf里面,然后将buf传给write,利用write进行输出

write系统函数

write
1.write: 2. mov eax, _NR_write 3. mov ebx, [esp + 4] 4. mov ecx, [esp + 8] 5. int INT_VECTOR_SYS_CALL

Write干的就是给寄存器传好参然后进入一个陷阱-系统调用

下面进入syscall函数进行刨析:

syscall
sys_call: ;ecx中是要打印出的元素个数 ;ebx中的是要打印的buf字符数组中的第一个元素 ;这个函数的功能就是不断的打印出字符,直到遇到:’\0’ ;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串 xor si,si mov ah,0Fh mov al,[ebx+si] cmp al,’\0’ je .end mov [gs:edi],ax inc si loop: sys_call .end: ret

Syscall在这里,只有一个功能:显示格式化了的字符串。

它将串里面的字节,从寄存器里面通过总线,复制到显卡显存里面,存放Ascll码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。最后我们的打印字符串就显示在了屏幕上。

8.4 getchar的实现分析

以下格式自行编排,编辑时删除

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

对个getchar的实现分析:

getchar
1. int getchar(void) 2. { 3. static char buf[BUFSIZ]; 4. static char *bb = buf; 5. static int n = 0; 6. if(n == 0) 7. { 8. n = read(0, buf, BUFSIZ); 9. bb = buf; 10. } 11. return(–n >= 0)?(unsigned char) *bb++ : EOF; 12. }

用户通过按下键盘,键盘的接口得到了代表按键的键盘码,产生了中断,这个中断,抢占了当前的进程,通过上下文切换机制,进入键盘中断子程序,子程序从键盘接口(文件),获得键盘按下的扫描码,让那后将这个扫描码转为ASCII码的形式,存到键盘的缓冲区里面。

对于getchar函数来说,调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。gethar对这个进行的是一次封装过程。即在按下回车以后读走一个字符。

8.5本章小结

本章具体讨论看hello的IO管理

通过分析说明Linux的IO设备管理方法/Unix
IO接口及其函数/printf的实现分析/getchar的实现分析

探讨了小小hello程序背后支撑着它的IO基础

(第8章1分)

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

逐条总结hello所经历的过程:

从被程序员,一个键盘,一个键盘的敲到文本文件里面,以ASCII码进行呈现,Hello便以文本形式进行了诞生

1. 通过预处理器的预处理,hello进行了补全,但仍然是C语言格式的

2. 被编译器编译后,成了汇编语言,文本格式,但是语言已经成立汇编的语言了。

继续向前,被汇编器处理成为了二进制代码,hello.o,这个文件的格式是ELF文件格式,有待于进一步被链接

链接器收到.o格式的hello,对hello进行符号解析与重定位。将最后已经链接成功的文件,输出到a.out里面。

我们在bash的shell里面,输入了执行命令./a.out,加载器替我们进行对hello进行加载,fork一个子进程,对子进程通过调用exceve函数,对虚拟内存进行了映射,hello就开始在子进程里面进行运行。

6. 在hello的main函数结束后,子进程进入终止状态,由父进程进行对子进程回收。

计算机系统的设计与实现的深切感悟

通过对小小的hello背后的计算机系统的探究,丰富了我对于hello的认识

表面上,只是一个小小的hello,但它的背后,是一群强大的抽象。

从指令集的ISA,到虚拟内存系统,再到进程的管理。

背后,是一个复杂的系统

一环套一环,为抽象进行服务。

搭起了计算机系统的高楼大厦

附件

列出所有的中间产物的文件名,并予以说明起作用。

文件名 介绍

hello.c 原始的hello文件(C语言)

hello.i 经过预处理之后的hello文件

hello.s 经过编译之后的hello文件,由汇编代码写成

hello.o hello经过汇编之后生成的可重定位的目标文件(二进制代码)

hello hello.o经过链接之后生成的目标文件,可以执行

参考文献

[1] CSAPP深入理解计算机系统第三版

[2] 程序员的自我修养-链接装载与库

[3] https://www.cnblogs.com/diaohaiwei/p/5094959.html.

[4] https://www.cnblogs.com/pianist/p/3315801.html

哈尔滨工业大学CSAPP大作业程序人生相关推荐

  1. 哈尔滨工业大学计算机系统大作业--程序人生

    计算机系统   大作业 题     目  程序人生-Hello's P2P      专       业   计算机科学与技术        学    号        2021110xxx      ...

  2. 哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P

    目录 摘要 第1章 概述 1.1 Hello简介 1.1.1 P2P:From Program to Process 1.1.2 020:From Zero-0 to Zero-0 1.2 环境与工具 ...

  3. 哈尔滨工业大学计算机系统大作业-程序人生 Hello‘s P2P

    文章目录 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的作用及概念 2.2 在Ubuntu下预处理的命令 2.3 Hel ...

  4. 哈尔滨工业大学计算机系统大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 计算机科学与技术学院 2021年5月 摘  要 本文以hello程序从hello.c到进程各个阶段所 ...

  5. 哈工大2022春CSAPP大作业-程序人生(Hello‘s P2P)

    摘  要 本论文研究了hello.c这一简单c语言文件在Linux系统下的整个生命周期,以其原始程序开始,依次深入研究了编译.链接.加载.运行.终止.回收的过程,从而了解hello.c文件的" ...

  6. 哈尔滨工业大学csapp大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 英才学院 学   号 7203610606 班   级 2036014 学       生 xxx 指 导 教 ...

  7. CSAPP大作业程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 班   级 学       生 指 导 教 师 吴锐 计算机科学与技术学院 2022年5 ...

  8. hit csapp大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学   号 2021112810 班   级 2103103 学       生 肖芩芩 指 ...

  9. 哈工大csapp大作业程序人生

    大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学    号 2021111719 班    级 2103101 学       生 杨济荣 指 导 教 ...

最新文章

  1. linux shell 加、减、乘、除
  2. 网站排名不稳定要从多方面进行分析
  3. Sublime P4语法高亮设置
  4. AcWing 831. KMP字符串(模板)
  5. ajax加载进度百分比,在ajax中显示加载百分比的进度条,php
  6. lwip网络通信socket_lwIP在Socket模式下接口:BSD Socket API
  7. centos7/rhel7下安装redis4.0集群
  8. Mr.J-- jQuery学习笔记(十五)--实现页面的对联广告
  9. 关于Android中的SlidingMenu中的用法
  10. 什么是Bootstrap?
  11. 基础知识的困惑让BUG更隐蔽
  12. 2020软考论文想要拿高分,要避开这些坑!
  13. 下载qq付费音乐的demo
  14. 在ROS中使用行为树
  15. 如果你热爱编码,就应该少写代码
  16. 回看科技股泡沫:区块链崛起恰逢其时,相当于1996年的互联网
  17. Java用HttpClient爬大学英语四六级考试成绩查询接口
  18. 《财富的未来》——佐藤航阳读书笔记
  19. DP/最短路 URAL 1741 Communication Fiend
  20. ios开发——实用技术篇Block/KVO/通知/代理

热门文章

  1. 清华深圳研究生院计算机报录比,最新全国各大高校各专业考研报录比率.xls
  2. License授权实现功能菜单控制调研
  3. 揭秘MCN,新人做自媒体必看!
  4. 用Python的Flask框架写微信小程序及其管理网页后台(准备篇)
  5. 卓有成效的程序员 阅读笔记 第一部分
  6. ORACLE动态SQL 存储过程
  7. 中间人攻击——ettercap的使用
  8. 加速浏览器控件的创建
  9. python计算器简单代码_Python之三十行代码实现简易计算器
  10. Velocity开发指南-内容