计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号
班 级
学 生
指 导 教 师
计算机科学与技术学院
2021年5月
本文阐述了hello程序的一生,详细描述了Linux系统下hello.c从预处理、编译、汇编到可重定位的目标文件,再链接经过符号解析和重定位之后到可执行文件的过程。同时详细介绍了hello的进程、存储和IO管理,从hello程序的一生中,梳理阐述了计算机系统的相关知识。
关键词:hello程序;预处理;编译;汇编;链接;进程;存储;IO管理
目 录
第1章 概述
1.1 Hello简介
Hello的P2P过程:
P2P,即Program to Process;Hello的P2P即Program hello.c to Process
具体过程为:
- linux下cpp预处理器将hello.c文件进行预处理,插入对应系统头文件中的内容,得到hello.i;
- 编译器ccl将其译为汇编语言文件hello.s;
- 汇编器as负责将汇编语言翻译为机器指令代码,得到hello.o文件;
- 链接器ld将hello.o和其它用到的预编译好的目标文件合并到一起并且完成引用的重定位工作,得到可执行文件hello。
(5)shell中输入./hello执行hello程序,调用fork函数创建子进程,并且在子进程中加载该程序,由此,hello.c文件就成为了一个进程。
Hello的020过程:
子进程调用execve加载hello执行,操作系统OS映射虚拟内存至物理内存,CPU为运行此进程分配时间,通过cache,TLB等机制加速访问时间,程序结束后,shell父进程通过相应的信号机制收到子进程结束信息并且回收子进程,内核将控制权转移回shell,子进程相关数据被清除。
1.2 环境与工具
硬件环境:CPU个数: 1 物理核数: 8 逻辑处理器个数: 16
具体信息如下图所示:
软件环境:VirtualBox 6.1.18 Ubuntu20.04.2LTS Windows10 64位
开发工具:VIM8.1.2269 Visual Studio Code1.54.3
1.3 中间结果
文件名 | 文件作用 |
hello.i | 预处理后的ASCII码文件 |
hello.s | 编译之后得到的文件 |
hello.o | 汇编之后得到的目标文件 |
hello | 链接之后得到的可执行文件 |
Hello.out | 反汇编之后得到的可重定位文件 |
helloELF.txt | 目标文件hello.o的ELF格式 |
helloELF1.txt | 可执行文件hello的ELF格式 |
1.4 本章小结
本章介绍hello.c的P2P和O2O过程,及其中的具体转换细节。并介绍了本次大作业的硬件、软件环境和使用到的开发工具,最后列举了完成大作业过程中生成的中间文件。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念:
预处理指程序编译c文件前,调用cpp将以字符“#”开头的命令解释为预处理指令,并且对c文件进行相应修改,包括加入源文件,处理宏定义、处理条件或语句删除注释等,将引用的所有库展开合并成为一个完整的文本文件。
2.1.2预处理的作用:
(1)处理头文件:cpp预处理器将头文件插入对应指令的位置,从而修改源文件。
(2)处理宏定义:用实际值或者是代码去替换#define定义的宏。
(3)处理条件编译指令:#ifef等相关语句代表需要判断条件编译等情况。
2.2在Ubuntu下预处理的命令
指令:gcc hello.c -E -o hello.i
生成文件如下:
2.3 Hello的预处理结果解析
预处理后hello.c文件转化为hello.i文件,原本28行的代码被扩展为3060行,文件内容添加,对原文件中的宏进行了宏展开,并添加了头文件中的内容。原本的注释被删除,原本的代码被放置在3043-3060行,没有太大变化。
hello.i中包含调用头文件的地址,其在原有代码的基础上,将头文件stdio.h的内容引入,例如声明函数、定义结构体、定义变量、定义宏等内容:
2.4 本章小结
本章主要介绍了预处理的概念及作用,展示了Linux系统下的预处理指令的具体操作,通过分析得到的hello.i文本,了解的cpp预处理器在执行预处理指令时候进行的具体工作,包括插入头文件,扩展宏定义,删除注释等内容。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
编译就是把高级语言变成汇编语言或机器语言的过程,编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。
本次大作业中,我们需要让编译器将预处理之后得到的hello.i文本文件翻译成hello.s文本文件。
3.1.2编译的作用
编译主要是为了将高级语言翻译为机器语言一一对应的汇编语言,部分步骤如下:
语法分析:编译器对程序代码进行分析和分割,形成一些C语言所允许的记号,同时检查代码中是否生成了不规范记号,如果存在不规范表达就生成错误提示。然后分析语义并且生成符号表,将不符合语法规则的地方标出并且生成错误提示。
代码优化:将中间表示形式进行分析并转换为功能等价但是运行时间更短或占用资源更少的等价中间代码。
3.2 在Ubuntu下编译的命令
指令:gcc -S hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1数据
(1)字符串常量
我们发现,hello.c文件中有两处printf中的字符串,具体信息如下:
编译后得到的hello.s文件中我们可以找到常量字符串对应的存储位置,并用.LCO和.LC1代表字符串的首地址:
可以看到汉字变为以utf-8的格式进行编码,每个汉字3字节,包括中文感叹号也使用了utf-8编码,占3字节。
(2)整形常量
整形常量在hello.c文件中是以实数的形式存在,例如:
if(argc!=4){
for(i=0;i<8;i++){
这些常量在汇编代码中变成了立即数,与寄存器中的值相比较或存储在相应的寄存器中:
(3)局部变量
hello.c中的局部变量有int i,参数argc,argv
(1)在汇编代码中可以发现i被分配在运行时栈上,初始值为0
其值小于等于7时执行循环。
- 参数argc,argv
可以看到argc存储在寄存器edi中,argv存储在寄存器rsi中,使用时又存在了栈中,栈指针分别为%rbp-20和%rbp-32。
3.3.2赋值
对循环变量i进行赋值,并赋初值为0。
for(i=0;i<8;i++)
汇编代码如下:
3.3.3算术操作
在for循环中,int i进行了++操作
for(i=0;i<8;i++)
在对应的汇编代码中,通过add指令完成
3.3.4 关系操作
Hello.c中的关系操作:
(1)对于表达式i<8的值的判断,通过cmp指令和jle指令组合完成对于for循环体是否循环的判断,从而完成循环体的跳转。
(2)对于表达式if(argc!=4)的判断,通过cmpl和je组合判断完成if语句的跳转。
3.3.5 控制转移
Hello.c中的控制转移包括if分支和for循环,分别对应两次关系操作:
- if分支if(argc!=4)
如下图所示,将argc的值与4比较,如果不相等,跳转到L4,相等则顺序执行,控制转移通过jle实现:
- For循环
for循环在.L2处对循环变量i进行初始化,然后通过jmp跳转到.L3,在.L3中进行了循环终止条件的判断,如果i<=7,则跳转到循环体部分.L4,否则顺序执行。
3.3.6数组/指针/结构操作
在main函数中传递的参数出现了指针数组
char *argv[])每一个数组的元素都是一个char*类型的指针,可以指向一个字符串,而具体来说argv[0]指向文件名,argv[1] argv[2]分别指向命令行输入的第一个和第二个参数,在汇编代码中这个指针数组的首地址被存放在rsi中,也就是存放main函数的第二个参数的寄存器。
对于指针数组argv,其每一个元素的大小都是8字节,而且数组中的元素应是连续存放的,因此址偏移量也应该是8的倍数
从此处的代码我们可以验证这一点。
3.3.7函数操作
(1)Main函数
main函数汇编代码中有两个参数argc和argv分别被存放在寄存器rdi和rsi中:
对于argv的操作,其指针数组的基地址通过寄存器存放,将基地址加上8的倍数的偏移量就可以访问指针数组的其它成员指针,用时为了访问每一个指针成员指向的字符串,我们需要访问每一个指针指向的内存空间,即每个指针成员指向的字符串是存放在内存中的。
Main函数是由系统函数调用的,同时main函数也可以调用别的函数,但是需要遵从寄存器保护的规则和参数传递的规则。Main函数返回值是int类型的,存储在寄存器rax中,在需要返回时,先将rax的值设置为0,然后返回即可,对应于hello.c文件中的return 0.
结合之前的代码我们可以发现,rax虽然被用作传递返回值,但是在此之前都可以被main函数自由使用。
Printf函数
Hello.c中,调用了2次printf函数:
printf("用法: Hello 学号 姓名 秒数!\n");
printf("Hello %s %s\n",argv[1],argv[2]);第一次调用的汇编代码部分如下:
我们看到,第一次实际上是调用了puts函数,这是因为此次调用printf函数不需要传递额外参数,只需将内存中存储的字符串复制到屏幕上,因此编译器做了等价的替换。第二次调用的汇编代码如下:
总共传递了三个参数,所以无法使用puts函数替换,第一个参数是主体字符串的首地址,存放在rdi中,第二、第三个参数是替换的字符串的首地址,分别通过寄存器rsi和rdx传递。
Exit函数
Exit函数的源代码如下:
if(argc!=4){
printf("用法: Hello 学号 姓名 秒数!\n");
exit(1);
}
对应的汇编代码如下:
我们可以看到函数传递的参数存放在edi中,对应了源代码中的exit(1),说明传递的整数值直接作为函数退出的状态值。
Sleep函数
Sleep函数的源代码如下:
for(i=0;i<8;i++){
printf("Hello %s %s\n",argv[1],argv[2]);
sleep(atoi(argv[3]));
}
对应的汇编代码如下:
可以看到sleep函数通过edi传递了一个参数作为休眠时间的控制。
Getchar函数
Getchar函数的汇编代码如下:
此函数无参数,不需要通过寄存器进行参数传递,。
3.4 本章小结
本章介绍编译的概念、作用及具体实现过程。对hello.i文件进行编译得到hello.s文本文件,并逐行分析汇编代码,使我们更加深刻的了解了常量和变量是如何进行存储运算的,非线性执行的跳转语句是如何进行的,以及在实现跳转的基础上如何进行逻辑控制实现循环。在跳转的基础上,我们也了解了函数如何进行调用,调用函数时如何进行参数传递,包括这个过程中寄存器的使用方法。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:运行汇编器as,将汇编语言翻译成机器语言的过程称为汇编,得到可重定位目标文件。在本次大作业中,此过程就是将hello.s文件翻译成hello.o文件。
汇编的作用:汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将汇编程序(hello.s)翻译成机器语言指令目标文件(hello.o),把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,目标文件是一个二进制文件,它包含程序的指令编码。
4.2 在Ubuntu下汇编的命令
汇编指令:gcc -c hello.s -o hello.o
输入指令过程及生成文件如下:
4.3 可重定位目标elf格式
4.3.1readelf操作
用readelf指令:readelf -a hello.o > helloELF.txt
将hello.o转换为TXT文档:
指令及生成文件如下:
4.3.2ELF头
ELF头的信息如下:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1240 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
可以看到,其中包含了包含了版本和系统信息、编码方式、节的大小和数量、ELF头大小和头部索引等等一系列信息。
4.3.3节头目表
节头目表包含了各个节的起始地址和偏移量等信息,具体内容如下:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000092 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000388
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000d8
0000000000000033 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 0000010b
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000137
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000138
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000158
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000448
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000190
00000000000001b0 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000340
0000000000000048 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000460
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
.rela.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.data节:已初始化的静态和全局C变量。类型为PROGBITS,意为程序数据,旗标为WA,即权限为可分配可写。
.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。
.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。类型为PROGBITS,意为程序数据,旗标为A,即权限为可分配。
.comment节:包含版本控制信息。 .note.GNU_stack节:用来标记executable stack(可执行堆栈)。
.eh_frame节:处理异常。 .rela.eh_frame节:.eh_frame的重定位信息。
.shstrtab节:该区域包含节区名称。 .symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
4.3.4重定位节
汇编器遇到对最终位置未知的目标引用,就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。
重定位节的内容如下:
Relocation section '.rela.text' at offset 0x388 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000021 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
00000000002b 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
000000000054 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 22
00000000005e 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
000000000071 000f00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000078 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000087 001100000004 R_X86_64_PLT32 0000000000000000 getchar - 4
Relocation section '.rela.eh_frame' at offset 0x448 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
可以看到,重定位节中有各种引用的外部符号,给出了他们的偏移量。我们还可以看到重定位节中的外部符号包括了全局变量,以及调用的函数。
有了这些信息之后,在下一步进行链接,就可以通过重定位节对这些位置的地址进行重定位,使其映射到虚拟内存上。
其中,重定位PC相对引用重定位算法如下:
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr);
重定位绝对引用重定位算法如下:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
4.3.5符号表
符号表的内容如下:
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 146 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar
No version information found in this file.
可以看到,其中包括了引用的函数,全局变量等等
4.4 Hello.o的结果解析
(以下格式自行编排,编辑时删除)
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.4.1 objdump操作
指令:objdump -d -r hello.o > hello1.txt
输入指令及生成的文件如下:
得到的反汇编代码如下:
4.4.2汇编代码比较
对两份汇编代码进行对比,我们可以发现以下的不同之处:
(1)立即数进制不同
Hello.s中的操作数采用10进制编码;
反汇编文件中的操作数用16进制编码。
(2)跳转语句不同
Hello.s中的跳转采用跳转到.L2等方式进行跳转;
反汇编文件中采用直接跳转计算出地址,跳转位置为主函数起始地址加上偏移量。
(3)函数调用机制不同
Hello.s中的函数调用采用跳转到函数名的方式进行跳转;
反汇编文件中函数调用采用直接跳转,跳转位置为主函数起始地址加上偏移量。
(4)有无对应机器码
hello.s:只有汇编代码
反汇编代码:有对应的机器码
4.5 本章小结
本章内容主要介绍了汇编的概念、作用和基本流程,了解到汇编语言文件是如何通过汇编器翻译成机器可识别的机器语言的,分析了直接编译hello.c文件得到hello.s中的汇编代码和反汇编hello.o文件得到的汇编代码的区别。使我们更加深刻的理解了汇编的基本机制。
第5章 链接
5.1 链接的概念与作用
5.1.1链接器的概念
链接就是将前一步得到的各种不同文件的代码段和数据段重新整合成两个片段,并且完成相应的符号解析和重定位的,将其组织成一个可执行文件的过程。,这个文件可以被加载到内存并执行。
5.1.2链接器的作用
把可重定位目标文件和命令行参数作为输入,产生一个完全链接的,可以加载运行的可执行目标文件,使得分离编译成为可能。,并且在链接的过程中,会合并一些提前预制编译的模块,比如printf.o,这也使得程序的运行效率提升。
5.2 在Ubuntu下链接的命令
命令如下:
生成文件如下:
5.3 可执行目标文件hello的格式
采用指令如下:
生成文件如下:
ELF的文件头如下所示:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4010f0
Start of program headers: 64 (bytes into file)
Start of section headers: 14208 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
可以看到里面的信息只要有系统版本,框架,大小,起止位置,此节大小等信息。
节头信息如下:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000400300 00000300
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.ABI-tag NOTE 0000000000400320 00000320
0000000000000020 0000000000000000 A 0 0 4
[ 4] .hash HASH 0000000000400340 00000340
0000000000000038 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 0000000000400378 00000378
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000400398 00000398
00000000000000d8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400470 00000470
000000000000005c 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000004004cc 000004cc
0000000000000012 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000400500 00000500
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000400530 00000530
0000000000000090 0000000000000018 AI 6 21 8
[12] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000070 0000000000000010 AX 0 0 16
[14] .plt.sec PROGBITS 0000000000401090 00001090
0000000000000060 0000000000000010 AX 0 0 16
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 0000000000401238 00001238
000000000000000d 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000402000 00002000
000000000000003b 0000000000000000 A 0 0 8
[18] .eh_frame PROGBITS 0000000000402040 00002040
00000000000000fc 0000000000000000 A 0 0 8
[19] .dynamic DYNAMIC 0000000000403e50 00002e50
00000000000001a0 0000000000000010 WA 7 0 8
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000404048 00003048
0000000000000004 0000000000000000 WA 0 0 1
[23] .comment PROGBITS 0000000000000000 0000304c
000000000000002b 0000000000000001 MS 0 0 1
[24] .symtab SYMTAB 0000000000000000 00003078
00000000000004c8 0000000000000018 25 30 8
[25] .strtab STRTAB 0000000000000000 00003540
0000000000000158 0000000000000000 0 0 1
[26] .shstrtab STRTAB 0000000000000000 00003698
00000000000000e1 0000000000000000 0 0 1
可以看到,节头描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。
程序头表信息如下:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000004002e0 0x00000000004002e0
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000005c0 0x00000000000005c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000245 0x0000000000000245 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x000000000000013c 0x000000000000013c R 0x1000
LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001fc 0x00000000000001fc RW 0x1000
DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001a0 0x00000000000001a0 RW 0x8
NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000320 0x0000000000400320 0x0000000000400320
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001b0 0x00000000000001b0 R 0x1
其中,每种结构都描述了系统准备程序执行所需的段或其他信息。
PHDR部分负责保存程序头表;
INTERP指定了在程序映射到内存之后必须调用的解释器;
LOAD表示一个从二进制文件映射到虚拟地址空间的段,其中保存了常量数据、程序的目标代码等等数据;
DYNAMIC保存了由动态链接器使用的信息;
NOTE保存辅助信息;
GNU_STACK是其中的权限标志,用于标识栈是否是可执行的;
GNU_RELRO则是在指定在重定位结束之后哪些内存区域需要设置为只读区域。
重定位节:
重定位条目告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。
重定位节的内容如下:
Relocation section '.rela.dyn' at offset 0x500 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000403ff0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000403ff8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section '.rela.plt' at offset 0x530 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000404018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000404020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000404028 000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0
000000404030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 atoi@GLIBC_2.2.5 + 0
000000404038 000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0
000000404040 000800000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
符号表:
Symbol table '.symtab' contains 51 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004002e0 0 SECTION LOCAL DEFAULT 1
2: 0000000000400300 0 SECTION LOCAL DEFAULT 2
3: 0000000000400320 0 SECTION LOCAL DEFAULT 3
4: 0000000000400340 0 SECTION LOCAL DEFAULT 4
5: 0000000000400378 0 SECTION LOCAL DEFAULT 5
6: 0000000000400398 0 SECTION LOCAL DEFAULT 6
7: 0000000000400470 0 SECTION LOCAL DEFAULT 7
8: 00000000004004cc 0 SECTION LOCAL DEFAULT 8
9: 00000000004004e0 0 SECTION LOCAL DEFAULT 9
10: 0000000000400500 0 SECTION LOCAL DEFAULT 10
11: 0000000000400530 0 SECTION LOCAL DEFAULT 11
12: 0000000000401000 0 SECTION LOCAL DEFAULT 12
13: 0000000000401020 0 SECTION LOCAL DEFAULT 13
14: 0000000000401090 0 SECTION LOCAL DEFAULT 14
15: 00000000004010f0 0 SECTION LOCAL DEFAULT 15
16: 0000000000401238 0 SECTION LOCAL DEFAULT 16
17: 0000000000402000 0 SECTION LOCAL DEFAULT 17
18: 0000000000402040 0 SECTION LOCAL DEFAULT 18
19: 0000000000403e50 0 SECTION LOCAL DEFAULT 19
20: 0000000000403ff0 0 SECTION LOCAL DEFAULT 20
21: 0000000000404000 0 SECTION LOCAL DEFAULT 21
22: 0000000000404048 0 SECTION LOCAL DEFAULT 22
23: 0000000000000000 0 SECTION LOCAL DEFAULT 23
24: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
25: 0000000000000000 0 FILE LOCAL DEFAULT ABS
26: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
27: 0000000000403e50 0 OBJECT LOCAL DEFAULT 19 _DYNAMIC
28: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
29: 0000000000404000 0 OBJECT LOCAL DEFAULT 21 _GLOBAL_OFFSET_TABLE_
30: 0000000000401230 5 FUNC GLOBAL DEFAULT 15 __libc_csu_fini
31: 0000000000404048 0 NOTYPE WEAK DEFAULT 22 data_start
32: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
33: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 _edata
34: 0000000000401238 0 FUNC GLOBAL HIDDEN 16 _fini
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
36: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
37: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 22 __data_start
38: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@@GLIBC_2.2.5
39: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
40: 0000000000402000 4 OBJECT GLOBAL DEFAULT 17 _IO_stdin_used
41: 00000000004011c0 101 FUNC GLOBAL DEFAULT 15 __libc_csu_init
42: 0000000000404050 0 NOTYPE GLOBAL DEFAULT 22 _end
43: 0000000000401120 5 FUNC GLOBAL HIDDEN 15 _dl_relocate_static_pie
44: 00000000004010f0 47 FUNC GLOBAL DEFAULT 15 _start
45: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
46: 0000000000401125 146 FUNC GLOBAL DEFAULT 15 main
47: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@@GLIBC_2.2.5
48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5
49: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@@GLIBC_2.2.5
50: 0000000000401000 0 FUNC GLOBAL HIDDEN 12 _init
可以看到,可执行目标文件的符号表表项数目明显多于可重定位目标文件的表项数目。可执行目标文件中加入了与调试、加载、动态链接相关的节,使得表示节的符号数增多。
动态符号表
动态符号表保存与动态链接相关的导入导出符号不包括模块内部的符号。
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (2)
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
可以看到程序头的部分信息如下:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000004002e0 0x00000000004002e0
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000005c0 0x00000000000005c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000245 0x0000000000000245 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x000000000000013c 0x000000000000013c R 0x1000
LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001fc 0x00000000000001fc RW 0x1000
DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001a0 0x00000000000001a0 RW 0x8
NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000320 0x0000000000400320 0x0000000000400320
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001b0 0x00000000000001b0 R 0x1
从中可知虚拟空间从0x400000开始。
在edb中打开hello程序,可以看到hello加载到虚拟地址的情况和各段的信息。在0x401000~0x402000段中,程序被载入,虚拟地址0x401000开始,到0x401ff0结束
根据节头目表,可以在edb中找到各个节的信息
例如可以看到.text节起始地址为0x4010d0,即可从edb中找到.text节的信息:
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.5.1hello反汇编指令与操作
指令如下:
objdump -d -r hello > hello2.txt
生成文件如下:
5.5.2hello与hello.o的反汇编文件的不同
(1)反汇编节数不同
在hello的反汇编文件中新增了.init和.plt节,和节中定义的函数。
(2)文件中包含的函数不同
在hello.c中调用的一些库函数被链接器链接到了hello文件中,通过反汇编代码就可以查看到这些新增函数,如exit、printf、sleep、getchar等函数。
(3)重定位后的函数调用地址不同:
Hello的跳转和函数地址都变成了虚拟内存地址,这就是链接器的重定位的功能。而hello.o的反汇编代码中,没有相应的虚拟地址,因此在.rela.text节中为其添加了重定位条目。
(4)虚拟地址的不同
hello.o中的代码段的起始地址为0,需要将其映射到对应的可执行文件的虚拟地址中,因此需要重定位,并且添加重定位条目。
5.5.3hello重定位地址计算
重定位PC相对引用重定位算法如下:
refaddr = ADDR(s) + r.offset;
*refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr);
重定位绝对引用重定位算法如下:
*refptr = (unsigned) (ADDR(r.symbol) + r.addend
其中,假设算法运行时,链接器为每个节(用ADDR(s)表示)和每个符号都选择了运行时地址(用ADDR(r.symbol))表示。
5.6 hello的执行流程
①开始执行:_start、_libc_start_main
②执行main:_main、_printf、_exit、_sleep、_getchar
③退出:exit
可以使用gdb进行逐步调试,得到每一步的地址值:
401000 <_init>
401020 <.plt>
401080 <puts@plt>
401090 <printf@plt>
4010a0 <getchar@plt>
4010b0 <exit@plt>
4010c0 <sleep@plt>
4010d0 <_start>
401105 <main>
401190 <__libc_csu_init>
401200 <__libc_csu_fini>
401208 <_fini>
5.7 Hello的动态链接分析
动态链接指在可执行文件装载时或运行时,由操作系统的装载程序加载库。大多数操作系统将解析外部引用(比如库)作为加载过程的一部分。其基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
延迟绑定是通过GOT和PLT实现的,根据hello ELF文件可知,GOT起始表位置为0x404000如下:
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
在edb中找到0x404000位置,发现GOT表位置在调用dl_init之前0x404008后的16个字节均为0:
在dl_init后条目改变了:
5.8 本章小结
本章主要介绍在Linux系统中链接的运行机制。介绍了将目标文件hello.o链接为hello可执行文件的过程,这也使我们更加充分的了解了链接器的工作细节。同时我们通过Ereadelf指令得到了ELF文件,以及objdump指令得到了反汇编文件,并前几章中的反汇编文件进行比较,使我们也更加清楚了关于链接和重定位的具体细节,以及动态链接的具体工作内容。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
6.1.2进程的作用
进程给应用程序提供一种假象:每个进程在运行时,就像是一个独立的逻辑控制流一样,独占整个CPU和内存以及其它资源。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:Linux系统中,Shell是一个交互型应用级程序,为使用者提供操作界面,它接收用户命令,然后调用相应的应用程序。
Shell-bash的处理流程:
(1)首先,Shell程序根据空格符解析输入的命令行,将其拆分为单个单词组成的字符串数组。
(2)shell程序解析得到结果,分析是否是合法命令。
如果不是合法命令则清空输入缓冲区,然后向屏幕输出错误信息或者帮助信息。如果是一个合法命令那么程序会接着判断这是一个内嵌命令或者是一个打开可执行程序的命令,如果是一个内嵌命令那么程序会立即执行这个命令,然后清空输入缓冲区,来到下一个循环,等待新的命令输入。
- 调用fork函数创建个子进程;
- 在子进程中调用execve函数加载输入的程序,如果程序能够正常打开就不会返回,知道程序运行结束,如果程序打开失败,execve函数返回。
- 回收子进程
在加载了程序之后,父进程shell会根据输入的命令行判断该程序是在前台执行还是后台执行,如果是在前台执行的话shell就会等待子进程的结束,然后将其回收,接着等待下一次命令行的输入。如果是在后台运行,那么shell程序就会立即开始等待下一次命令行的输入,不再管当前子进程,知道子进程暂定或是终结,父进程shell程序收到信号,才会对子进程进行回收。
6.3 Hello的fork进程创建过程
(1)输入命令./hello后,父进程shell会先解析命令,判断这是一个非内嵌的前台执行的命令。
(2)调用fork函数,创建的子进程,且子进程虚拟空间的内容与父进程完全相同,还包括相同的打开文件:这意味着父进程打开的文件,子进程拥有完全一样的读写权限。二者的不同之处在于其虚拟内存空间指向的物理内存空间不同,而且其进程ID,也就是PID也不同。此外,fork函数被调用一次,但是会分别在父子进程中各返回一次,共计两次,父进程返回子进程的PID,子进程返回0。
6.4 Hello的execve过程
shell父进程创建的子进程,子进程调用execve函数,其参数主要是两个二级指针char **argv , char **envp,具体步骤如下:
(1)删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
(2)execve加载Hello,会为新的程序映射虚拟内存空间,并且将控制流转移到新程序的主函数中,主函数main有三个参数int argc , char **argv , char **envp。
(3)子进程还会为新的程序映射新的数据段和代码段区域,以及共享区。
(4)程序计数器(PC)的值修改为新程序的代码区域入口处的地址。
6.5 Hello的进程执行
6.5.1逻辑控制流
逻辑控制流即控制程序的逻辑行为。控制流一般分成正常控制流和异常控制流,正常就是一切按预期的方向发展,异常就是控制流的突变。
6.5.3上下文
上下文是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等构成。此外, 还包括进程打开的文件描述符等。
6.5.4调度的过程
系统中用户进程数一般多于处理机数,这导致它们互相争夺处理机。这就要求进程调度程序按一定的策略,动态分配处理进程,使之合理执行。
进程执行时,内核可以决定先停止当前进程的运行,然后重新启动一个先前以及暂停了的进程,这种决策就叫做进程的调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
进程之间的调度示意图如下:
6.5.5用户态与核心态转换
为了能让处理器安全运行,我们需要对于程序对于不同区域的读写权限进行划分。
根据权限的不同将模式划分为用户模式与核心模式。
核心模式拥有最高的访问权限,何以访问和修改任意处的数据,处理器用一个寄存器来记录当前的模式。对于一个进程,只有在陷入故障,中断或者是系统调用等情况的时候才会进入内核模式,其他时候都始终处于用户模式之下,无法对系统或者是内核的核心数据和代码作出修改,从而保证了操作系统的安全性。
6.6 hello的异常与信号处理
6.6.1异常的类型和处理流程
深入理解计算机系统书中主要总结了4种类型的异常和相应的处理流程:
中断
中断类型的异常来源于I/O设备的信号,是一种异步的异常,具体的处理流程如下:
可以看到其包括四个步骤并总是返回下一条指令。
陷阱
陷阱是一种进程故意达成的状态,目的是为了进行系统调动,进入内核模式,其异常是同步的,具体处理流程如下:
故障
产生故障的原因是因为当前进程发生了一些错误,而这种错误有可能可以被修复(例如缺页错误),也有可能不能被修复。这种异常是同步的,具体处理流程如下:
处理程序要么处理故障成功,返回当前导致故障的指令使其重新执行;要么处理故障失败,程序报错并且终止。
终止
程序终止的原因是因为发生了一个不可以被修复的错误,这种异常是同步的,且不会返回,具体流程如下:
6.6.2正常运行状态
输入运行命令和正确参数,程序正常运行的结果如下:
程序按照指定时间间隔,执行八次循环输出指定信息。
6.6.3按下Ctrl+Z:
- 按下Ctrl+Z之后,进程会收到SIGSTP 信号,使得当前的hello进程被挂起。
- 用ps指令查看其进程PID,可以发现hello的PID是9294;
- 用jobs查看此时hello的后台 job号是1;
- 调用指令fg 1将其调回前台。
6.6.4按下Ctrl+C
- 按下ctrl C后,此时进程收到一个SIGINT 信号,一次结束 hello。
- 输入ps指令,发现查询不到hello进程的PID。
- 输入指令jobs,发现也没有对应作业,hello进程被彻底终止。
6.6.5中途乱按:
- 中途乱按程序会将其记录在输入缓冲区,不会影响程序的运行;
- 如果在hello运行结束后还有乱按的指令在缓冲区的,那么就会被作为新的命令行输入。
6.6.6Kill命令:
- kill指令向所在的挂起的进程发出终止指令;
- ps指令无法找到对应的进程;
- jobs指令也无法找到作业,进程已经被终止。
6.7本章小结
本章进一步介绍了hello作为进程运行时候的相关知识。包括shell程序中hello进程是如何被创建、加载和结束的,通过键盘输入的指令是如何在输入缓冲区等待和清除的,以及在hello进程执行的过程中,是如何捕获信号并且对信号作出相应的反应和行为的。对于不同的信号,有着不同的处理方式和结果。除此之外还介绍了如何在shell进程中查看进程和作业,以及如何设置前台或者是后台作业。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。 是指由程序产生的与段相关的偏移地址部分。
7.1.2线性地址
线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
7.1.3虚拟地址
虚拟地址是程序运行在保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址。
7.1.4物理地址
物理地址指放在寻址总线上的地址。
放在寻址总线上如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址的构成有两个部分:分别是段标识符和段内偏移量。其中,段标识符是一个由16位长数据组成的字段,将其称为段选择符。其中的前13位是一个索引号。而后面三位中则包含了一些硬件的细节。
段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。
在x86保护模式下,段的信息即段描述符占8个字节,段信息无法直接存放在段寄存器中。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值。
- 首先给定一个完整的逻辑地址;
- 其次,看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段;
- 再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。
- 接着,拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。
- 最后,把基地址Base+Offset,就是要转换的下一个阶段的地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
(1)首先,将线性地址划分为VPN+VPO的格式;
(2)然后将VPN拆分为TLBT+TLBI的格式,然后在TLB中寻找对应的PPN,如果有缺页的情况发生,那么就去下一级页表中寻找对应的PPN;
(3)以此类推,找到PPN之后,将其与之前的VPO进行组合就得到了对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
多级页表将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。
在k级页表的情况下,会将VPN的部分分成等长的k个段,将每一个段作为某一级页表的索引,直到找到正确的PPN。
多级页表示意图如下:
7.5 三级Cache支持下的物理内存访问
(1)首先使用物理地址的CI进行组索引,对8个块分别对CT进行标志位的匹配。如果匹配成功且块的有效位为1,则成功命中。然后根据数据偏移量 CO取出相应的数据并返回。这里的数据保存在一级Cache。
(2)如果没有命中,或者没找到相匹配的标志位,那么就会在下一级Cache中寻找,只要本级Cache中没找到就要去下一级的Cache中寻找数据,然后逐级写入Cache。
(3)在更新Cache的时候,首先需要判断是否有有效位为0的块。若有,则直接写入;若不存在,则需要驱逐一个块(LRU策略),再进行写入。
Cache示意图如下:
7.6 hello进程fork时的内存映射
(1)fork函数被当前进程调用时:
首先,内核会为新进程创建各种与父进程相同的数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。
(2)fork在新进程中返回时:
新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个在之后进行写操作时,写时复制机制就会为其创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。
7.7 hello进程execve时的内存映射
在exceve函数加载和执行hello程序的过程中,需要执行几个步骤:
- 首先删除已存在的用户区域。
- 然后,会映射私有区域,该函数会为Hello的代码、数据、bss和栈区域,并创建新的区域结构,这些区域都是私有、写时复制的。
- 接着,.映射共享区域,比如,在Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的。最后再设置程序计数器,完成相应的跳转。
7.8 缺页故障与缺页中断处理
如果在页表中发生了一次不命中,我们就认为发生了一次缺页故障,具体处理流程如下:
- 首先,由于PTE中对应页的有效位是0,所以MMU出发了一次缺页异常信号,此时操作系统就开始调用相应的异常处理程序。
- 被调用的缺页处理程序会根据一定的替换策略确认出物理内存中的牺牲页,如果这个页已经被修改了,程序就会把它换到磁盘。
- 缺页处理程序在物理内存中写入新的页面,并更新内存中的PTE。
- 完成了上述操作之后,缺页处理程序就会返回到原来的进程,再次执行刚才导致缺页的命令。此时,因为虚拟页面已经换存在物理内存中,所以就会命中。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存空间,其要么是已分配的,要么是空闲的。动态内存分配主要有两种基本方法与策略:
(1)隐式空闲链表
隐式空闲链表通常带有一个头部和脚部标签,内容相同,记录块的大小和是否分配等信息。一般来说每个块都是由头部和脚部、有效载荷、可能的额外填充组成,对于某些优化的链表,已分配的块可以没有脚部。
在隐式空闲链表中,所有的块都是通过头部和脚部中的大小字段连接着的。因此分配器可以依次遍历整个堆。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。
当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。
(2)显示空间链表管理
显式空闲链表是将空闲块组织为某种形式的显式数据结构。堆被组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。
在显式空闲链表中,可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表。其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
7.10本章小结
本章主要介绍了虚拟内存的基本思想和基本机制,以及如何通过虚拟地址得到一个物理地址,如何通过TLB机制来加速寻找的过程。我们介绍了关于堆的动态分配的不同机制,关于空闲块管理和分配的机制的不同我们可以得到不同的效果的堆分配器。还介绍了一个新的进程在被创建或者是一个程序被加载时,系统如何为其分配和创建存储空间,以及私有文件和共享文件在存储和读写时候的不同机制。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.1.1设备的模型化
我们可以将所有的I/O设备都被模型化为文件,方便统一进行操作。
8.1.2设备管理
这种将设备统一模型化为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这样,我们就可以简单地对所有的文件执行open/close,read/write或是lseek操作等等。
8.2 简述Unix IO接口及其函数
8.2.1Unix IO接口
打开文件:
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
linux shell:
创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件中的常量可以代替显式的描述符值。
改变当前的文件位置:
对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用 程序能够通过执行 seek,显式地将改变当前文件位置 k。
读写文件:
一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件:
当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2.2Unix IO函数
open函数
此函数用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。如果打开成功就返回其文件描述符,如果失败则返回-1
close函数
该函数用于关闭一个被打开的的文件,该文件通过输入参数文件描述符fd确定,如果成功关闭则返回0,如果关闭失败则返回-1。
read函数
该函数带有缓冲区,用于从文件读取数据。返回读取到的文件字节数,如果直接读取到EOF就返回0,读取失败返回-1。
write函数
该函数用于向文件写入数据,如果写入成功就返回向其写入的字节数,写入失败就返回-1。
lseek函数
该函数用于将文件描述符指定的文件的指针位置移到别处中,如果成功则返回当前位置,失败就返回-1。
8.3 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;
}
printf函数是格式化输出函数, 一般用于向标准输出设备按规定格式输出信息。printf中调用了两个函数,分别为vsprintf和write:
其中引用的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++;
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函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度。
write函数是将buf中的i个元素写到终端的函数。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar源码如下:
int getchar(void)
{
static char buf[BUFSIZ];
static char *bb = buf;
static int n = 0;
if(n == 0)
{
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(--n >= 0)?(unsigned char) *bb++ : EOF;
}
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换为ASCII码,保存到系统的键盘缓冲区中。
getchar函数调用了read函数,read函数也通过sys_call调用内核中的系统函数,将读取存储在键盘缓冲区中的ASCII码,直到回车符,然后返回整个字符串,getchar函数只从中读取第一个字符,其他的字符被缓存在输入缓冲区。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
8.5本章小结
本章介绍了Linux的I/O设备的抽象处理和读取写入等操作,深入地剖析了关于文件描述符以及文件打开,文件位置等机制,了解了Unix I/O接口和函数,并且分析了printf和getchar函数是如何通过Unix I/O函数来实现的。
结论
hello经历的过程:
- 源程序编写:通过编写工具(文本编辑器、IDE等)编写出hello.c;
- 预处理:预处理器cpp读取需要的系统头文件内容,生成ASCII码的中间文件hello.i。
- 编译:编译器ccl将C语言代码翻译成汇编指令,生成hello.s。
- 汇编:汇编器as将hello.s翻译成机器语言指令,并生成重定位信息,将结果保存在可重定位目标文件hello.o中。
- 链接:链接器进行符号解析、重定位、动态链接等创建一个可执行目标文件hello,此时hello可以被执行。
- 运行阶段:当我们在shell键入./hello启动程序的时候,shell调用fork函数为其产生子进程,子进程中调用execve函数,加载hello程序,进入hello的程序入口点。
- 进程运行:内核负责调度进程,并对可能产生的异常及信号进行处理。内存的管理由MMU、TLB、多级页表、cache、DRAM内存、动态内存分配器共同完成,而Unix I/O的作用则是让程序与文件进行交互。
- 终止:hello最终被shell父进程回收,内核删除为hello进程创建的所有数据结构。
感悟:
这次大作业带给我在实践中理解计算机系统的一次机会,以往在课堂中学习总是忙着对知识的记忆,而知识背后的应用却被我忽视了。而完成这次大作业的过程中,随着对hello程序一生的探索,也打开了我对计算机系统深入理解的大门,感受到了计算机系统之美。机器是没有思维的,但是当我们探索机器背后的机理时,就仿佛在和一代代为计算机科学做出贡献的人进行思维上的交流,感受计算思维的美妙。
附件
文件名 | 文件作用 |
hello.i | 预处理后的ASCII码文件 |
hello.s | 编译之后得到的文件 |
hello.o | 汇编之后得到的目标文件 |
hello | 链接之后得到的可执行文件 |
Hello.out | 反汇编之后得到的可重定位文件 |
helloELF.txt | 目标文件hello.o的ELF格式 |
helloELF1.txt | 可执行文件hello的ELF格式 |
参考文献
- Bryant R E. O'Hallaron D R. Computer systems: A programmer's perspective[M]. Upper Saddle River: Prentice Hall, 2003.
- 桂盛霖, 徐洁. “计算机系统结构”实验教学探索[J]. 实验科学与技术, 2013,11(6): 306-308.
- 李山山, 郑宁汉, 高玉超. 基于开放式CPU设计系统的双核CPU设计[J]. 实验室科学,2015,18(5): 27-32
- 刘磊,熊小鹏.最小驻留价值缓存替换算法[J].计算机应用,2013,33(04):1018-1022.
[5]]黄毅伟. TLB的设计与验证[D].国防科学技术大学,2006.