本论文论述了关于对给定的hello.c源程序,将其在Linux系统下生成可执行文件hello的一系列过程。通过这一个看似简单却精妙的程序,详细地阐述了计算机在预处理,编译,汇编和链接时,内部的软件、硬件之间的复杂配合,包括进程切换、内存分配、输入输出、地址映射等。深入剖析hello.c的程序人生,能使得我们对于计算机系统这门课有一个更成体系的知识脉络。
关键词:Linux;计算机系统;预处理;编译;汇编;链接;进程;内存;IO。
目 录
2.2在Ubuntu下预处理的命令........................................................................... - 5 -
5.3 可执行目标文件hello的格式...................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程...................................................... - 10 -
6.3 Hello的fork进程创建过程...................................................................... - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理.............................................. - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理........................................ - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换............................................... - 11 -
7.5 三级Cache支持下的物理内存访问........................................................... - 11 -
7.6 hello进程fork时的内存映射................................................................... - 11 -
7.7 hello进程execve时的内存映射............................................................... - 11 -
7.8 缺页故障与缺页中断处理............................................................................ - 11 -
8.1 Linux的IO设备管理方法........................................................................... - 13 -
8.2 简述Unix IO接口及其函数........................................................................ - 13 -
第1章 概述
1.1 Hello简介
P2P:
1.1.1从键盘输入源文件信息
hello程序的生命周期是从一个源程序开始的,需要我们通过编辑器从键盘输入到系统中。保存之后系统将其以一串0和1组成的位的形式存入内存。
1.1.2 hello程序被编译
我们可以在Linux下的终端shell/bash输入命令gcc -o hello hello.c,来生成它的可执行文件hello。这里调用的是GCC编译器驱动程序,并且经过了四个阶段:预处理、编译、汇编、链接,分别使用了预处理器、编译器、汇编器、链接器。后续会逐个详细阐述。
1.1.3 hello 被终端加载并运行
生成可执行文件hello之后,输入命令./hello,终端会为其fork()一个子进程,子进程拥有hello的各种数据结构。此处进程之间的转换,即P2P。
020:
1.1.4 映射虚拟内存
Linux加载器execve将程序加载到新创建的子进程中,之后通过虚拟内存映射将程序从磁盘载入物理内存中执行,这其中包括段式管理、页式管理等等。
1.1.5 CPU分配时间片
CPU为该进程分配时间片,执行该程序对应的逻辑控制流。这使得我们的程序看起来好像在独占的使用处理器和内存系统。
1.1.6 hello子进程的回收
子进程return后终止,此时内核会发送一个SIGCHLD信号,被进程捕获后会回收相应的子进程,避免浪费内部资源。最后,有关hello的内存释放,到此,hello的生命周期结束。
1.2 环境与工具
硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk。
软件环境:Windows10 64位,Vmware11;Ubuntu20.04LTS 64位。
开发与调试工具:gcc,Codeblocks20.04,edb1.3.0。
1.3 中间结果
文件名 | 文件的作用 |
hello.c | 源程序文件(文本) |
hello.i | hello.c经过cpp预处理后生成的程序(文本) |
hello.s | hello.i经过cc1编译后产生的汇编程序(文本) |
hello.o | hello.s经过as汇编后产生的可重定位目标程序(二进制) |
hello | hello.o和其他可重定位目标程序以及一些必要的系统目标文件通过ld链接起来(二进制) |
hello.elf | hello.o文件的elf格式文件(文本) |
hello1.elf | hello文件的elf格式文件(文本) |
asm.txt | hello.o的反汇编代码文件(文本) |
asm1.txt | hello的反汇编代码文件(文本) |
1.4 本章小结
第一章主要介绍了hello.c程序的P2P和020的过程,作为剩下7章内容的一个汇总。还介绍了一些实验的环境和工具,和中间过程生成的文件。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:
预处理器(cpp)根据以字符#开头的命令(#include,#define,#undef等),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。比如hello.c中的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,这样就得到了另一个程序,通常以.i作为文件扩展名。
预处理的作用:
- 文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
- 条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
- 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
2.2 在Ubuntu下预处理的命令
Ubuntu下预处理的命令为:
cpp hello.c hello.i
或
gcc -E hello.c -o hello.i
图2-1 预处理指令及其运行结果
2.3 Hello的预处理结果解析
打开刚刚生成的hello.i文件,发现其内容得到了扩展,达到了三千多行,分析其内容,属于ASCII码中间文件。
图2-2 hello.i的头部内容
图2-3 宏展开后头文件被包含
2.4 本章小结
本章详细地介绍了预处理这一步骤的概念和作用。通过对比预处理之前后之后的文件内容,发现预处理可以大大减少我们编写程序时的工作量,更为简便地使用库和宏,其余的工作交给预处理器(cpp)即可。预处理在编写C程序的其他方面也有重大意义。
第3章 编译
3.1 编译的概念与作用
编译的概念:
编译就是将高级语言翻译成汇编语言或者机器语言的过程,通俗的说就是把高级语言程序员说的话翻译成机器能听懂的话。这里的编译指的就是利用编译器cc1将预处理得到的hello.i编译成汇编程序hello.s,其中进行了一系列的词法分析、语法分析、语义分析及优化。这是整个程序构建的最核心的部分,也是最复杂的部分。
编译的作用:
1.扫描代码,将源代码程序输入扫描器,将源代码的字符序列分割成一系列记号。
2.源代码优化,中间代码(语言)使得编译器分为前端和后端,前端产生与机器(或环境)无关的中间代码,编译器的后端将中间代码转换为目标机器代码,目的是使一个前端对多个后端,适应不同平台。
3. 代码生成及目标代码优化。代码生成依赖于目标机器,依赖目标机器的不同字长,寄存器,数据类型等。目标代码优化器进行目标代码优化,选择合适的寻址方式,左移右移代替乘除,删除多余指令。
3.2 在Ubuntu下编译的命令
Ubuntu下编译的命令为:
cc1 hello.i -o hello.s
或
gcc -S hello.c -o hello.s
(cc1不在path中需要手动添加路径或者将cc1改为/usr/lib/gcc/x86_64-linux-gnu/9/cc1)
Ubuntu下编译的运行结果为:
图3-1 编译指令及其运行结果
3.3 Hello的编译结果解析
3.3.1数据类型
1.常量
hello.s中的常量为printf()中打印的两个字符串,它们以UTF-8编码的格式存在只读的数据段.rodata中。
图3-2 汇编代码中的常量部分
2.变量
图3-3 源代码中主函数的变量i部分
从源程序中可以看出在main函数里声明了一个局部变量i,在编译器编译生成汇编语言后,i被存储在了-4(%rbp)的位置,如下图所示:
图3-4 汇编代码中主函数的变量i部分
其中.L3标签处是循环判断条件和主函数剩余部分对应的汇编代码。
除此之外,还有main函数的参数argc和argv(见图3-3),其中argv为指针数组。同时由寄存器规则可知,argc存储在了寄存器%edi(第一个参数)中,argv存储在了寄存器%rsi(第二个参数)中。图如下:
图3-5 汇编代码中主函数的参数部分
由上图可知,argc被存储在了-20(%rbp)的位置,argv被存储在了-32(%rbp)的位置。
3.3.2赋值语句
hello.c程序中的赋值操作为循环语句中的i=0,具体在汇编代码中的实现形式为movl $0, -4(%rbp),见图3-4。定义i时并未赋初值,见图3-3,但是编译器优化后在汇编代码中顺便赋了初值。
3.3.3类型转换
hello.c程序中涉及的类型转换为atoi(argv[3]),该语句是利用atoi函数将字符串类型显式转换为整数类型。
图3-6 源代码中主函数的类型转换部分
这一段对应于将argv[3]转换为整型数,需要先计算出其地址,并取内容放入寄存器%rdi中,后通过调用atoi@PLT函数,将%rdi中的内容作为参数传入,然后返回到%rax中,可以发现,在C语言中一句简洁的函数调用在汇编语言中实现起来是多么的复杂。
图3-7 汇编代码中主函数的类型转换部分
3.3.4算术运算
hello.c程序中的算术操作只出现了i++(见图3-6),又因为i为int类型,所以实现i++只需要用addl指令即可。具体实现指令为addl $1, -4(%rbp)。图如下:
图3-8 汇编代码中主函数的算术运算部分
每次对i加1之后检查i和7的大小关系,cmpl会在i小于等于7时设置条件码,使程序跳转到循环体.L4。
3.3.5关系操作
1.“!=”
hello.c中有判断语句argc != 4。见下图:
图3-9 源代码中主函数的关系操作“!=”部分
该语句用于判断argc的值是否与4相等,该语句被编译为cmpl $4, -20(%rbp)。比较完成后会设置条件码,通过条件码判断跳转位置(见图3-10)。
2.“<”
hello.c中有判断语句i<8,见图3-6。
该语句用于判断局部变量i和8的大小作为循环的条件,在汇编代码中被编译为cmpl $7, -4(%rbp)。通过比较完成后的条件码确定跳转位置。见图3-8。
3.3.6数据结构
1.数组
hello.c程序中的数组为argv[],其为一个字符串指针数组,具体分析见3.3.1中第2部分相关内容。
2.指针
argv[]中每个元素都是一个指向字符串的指针,在汇编语言中通过()访问地址中的内容,一个具体的例子请见图3-7第45行汇编代码。另外需要注意的是,在有的指令中,括号不一定是取地址中的内容,比如lea指令。
3.3.7控制转移
1.if语句
判断argc是否为4,若为4,则执行if体的语句,若不是,则不执行。具体是通过设置条件码寄存器实现的。对应的汇编代码见下图:
图3-10 源代码中主函数的if语句部分
2.for循环
for(i=0;i<8;i++){…}每次循环前判断i是否小于8,若满足,则执行循环体,否则不执行。循环判断条件见图3-8,循环体内容见下图:
图3-11 源代码中主函数的for循环循环体部分
3.3.8函数操作
1.main函数
main函数的参数为argc和argv。argc存储在%edi中,argv存储在%rsi中。返回值为int类型,存储在%eax中。详见3.3.1第2部分。返回值为0,见下图:
图3-12 源代码中主函数的返回值部分
2.printf函数
hello.c程序中调用了两次printf函数,但是两次调用传入的参数不同。第一次传入的是.LC0处的字符串,如下图:
图3-13 源代码中主函数第一次调用printf部分
第二次传入的是.LC1处的字符串以及argv[1]和argv[2],汇编代码具体实现如下:
图3-14 源代码中主函数第二次调用printf部分
第33到40行是将参数分别传入三个寄存器中,然后调用printf@PLT函数。
- exit函数
exit函数实现从main函数退出。hello.c程序中传入的参数是1,表示非异常退出。汇编代码具体实现如下:
图3-15 源代码中主函数调用exit部分
2. atoi函数
atoi函数实现将字符串类型的数据转变成int类型的数据,传入的参数为argv[3]。具体见图3-7。
3. sleep函数
sleep函数实现程序休眠,传入的参数为atoi(argv[3])(表示休眠秒数)。汇编代码具体实现如下:
图3-16 源代码中主函数调用sleep部分
此处将上面atoi函数返回值从%eax传入%edi,作为参数再传入函数sleep@PLT。
4. getchar函数
getchar函数实现读取缓冲区字符。不需要传递参数,直接调用即可。汇编代码具体实现如下:
图3-17 源代码中主函数调用getchar部分
3.4 本章小结
本章详细地阐述了编译的概念和作用,并且对hello.c的编译产生的hello.s中的汇编程序和C语言中的实现进行了详细对照。汇编语言虽然晦涩难懂,但是对于一个优秀的程序员来说,具备流畅阅读汇编代码的能力是必要的,因为它能在根源上阐释一些性能问题或者帮我们去修复bug。汇编语言的编写难度很大,所以编译器的作用就尤为突出了,这一章内容也是很好地印证了这一点。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:
把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。
汇编的作用:
将机器无法识别的汇编代码转换为可以识别的机器指令,使其在链接后能被机器识别并执行。为二进制的扩展名是.o的文件。
4.2 在Ubuntu下汇编的命令
Ubuntu下汇编的命令为:
as hello.s -o hello.o
或
gcc -c hello.s -o hello.o
图4-1 汇编指令及其运行结果
4.3 可重定位目标elf格式
这里采用指令
readelf -a hello.o > hello.elf
将hello.o的ELF格式重定位输出到hello.elf中。结果如下:
图4-2 生成hello.o的ELF格式文件hello.elf
根据CSAPP一书中P467的图7-3提到的典型的ELF可重定位目标文件的结构,我们可知从上至下依次为ELF头、.symtab、.rel.text、节头部表,但是打开hello.elf文件后我们发现和书上所提到的结构稍有不同,依次为ELF头、节头、重定位节、symbol table。下面将按照Ubuntu中生成的elf顺序来逐个讲解。
1.ELF头
ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型(.o\.out\.so)、机器类型、节头部表的文件偏移、节头部表的条目的大小和数量。
图4-3 ELF头
2到11行介绍的是系统和环境信息,12到20行介绍的是程序信息,包括程序头和节头的文件偏移和大小。
2.节头
记录各节名称、类型、地址、偏移量、大小、全体大小(Entry)、旗标、链接、信息、对齐。
图4-4 节头
发现其结构与CSAPP P469上基本一致,但由于hello.c源程序较为简单,很多节没有内容,所以其大小为0。
3.重定位节
.rela.text,保存的是.text节中在链接器把这个目标文件和其他文件组合时需要修正的信息。本程序需要被重定位的是函数printf、puts、exit、sleepsecs、getchar、sleep和.rodata中的字符串常量.L0和.L1。
图4-5 重定位节.rela.text
同理.rela.eh_frame节是.eh_frame节重定位信息。
图4-6 重定位节.rela.eh_frame
4.符号表
.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息,每个可重定位目标文件在.symtab中都有一张符号表。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。这里的符号表包含17个条目,是hello.c中使用到的函数和全局变量,结构和CSAPP P469的表格一致。
图4-7 符号表.symtab
4.4 Hello.o的结果解析
使用指令
objdump -d hello.o > asm.txt
将反汇编后生成的代码重定位生成到asm.txt中。如下图:
图4-8 反汇编生成asm.txt
打开asm.txt,观察并分析其内容,发现与hello.s的汇编代码有着几处较为明显的区别:
1.分支转移
在hello.s中,分支跳转是直接以.L0等助记符表示,但在反汇编代码中,程序没有使用助记符将其分段,分支转移表示为主函数+段内偏移量,更适合于被加载到内存中。
2.函数调用
hello.s中函数调用时直接给函数名称,但由于在编译阶段没有保留符号的名字,所以在反汇编的文件中call之后加main+偏移量(定位到call的下一条指令),即用具体的地址表示。在.rela.text节中为其添加重定位条目等待链接。说明机器语言用特定的字节表示各种操作。只要给定了文件的开始位置,就可以把合法的字节序列唯一地解释为有效的指令。
3.访问全局变量
汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip),因为访问时需要重定位,所以初始化为0并添加重定位条目。
图4-8 反汇编生成的asm.txt内容
4.5 本章小结
本章主要介绍了汇编过程的概念及作用,用输入指令的方式模拟了汇编器将.s文件转换成机器语言的可重定位目标文件.o。还详细解释了ELF格式的可重定位目标文件的各个节的具体意义。最后对比了反汇编后的汇编与.s的汇编语言的差异。汇编器接受汇编代码,产生可重定位目标文件。它可以和其他可重定位目标文件合并而产生一个可以直接加载被运行的可执行目标文件。正因为它并不包含最终程序的完整信息,它的符号尚未被确定运行时位置,并用0占位。在第五部分中将说明如何将多个可重定位目标文件合并,并确定最终符号的最终运行位置。
第5章 链接
5.1 链接的概念与作用
链接的概念:
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于运行时,也就是由应用程序来执行。
链接的作用:
链接在软件开发过程中不可或缺,它使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件。理解链接和链接器还有以下的几种作用:
理解链接器将帮助我们构造大型程序;
理解链接器将帮助我们避免一些危险的编程;
理解链接将帮助我们理解语言的作用域规则是如何实现的;
理解链接将帮助我们理解其他重要的系统概念;
理解链接将使我们能够利用共享库。
5.2 在Ubuntu下链接的命令
Ubuntu下链接的命令如下(因为很多驱动程序不在path导致命令较长,可手动添加到path以缩短命令):
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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro
图5-1 链接生成的可执行文件hello
5.3 可执行目标文件hello的格式
此处延续4.3中的方法。输入指令
readelf -a hello > hello1.elf
hello1.elf即为hello的elf格式文件。
图5-2 hello的ELF格式文件
5.3.1ELF头
图5-3 hello的ELF头
由上图可以看出,hello的ELF头中指出hello的类型为EXEC类型,即hello为可执行目标文件,同时hello的入口地址非零,说明重定位工作已完成。注意到hello的ELF头中program headers的偏移量非零,说明hello文件中比hello.o文件中多了一个段头表。同时hello文件中节头表的条目数量为27,比hello.o文件中的数目多,另外程序和节的起始位置和大小也发生了变化。这些数据的改变代表着链接工作的完成。
5.3.2节头
图5-4 hello的节头
由上图可以看出,hello中节头表的条目数多于hello.o中节头表的条目数。值得注意的是每一节都有了实际地址,而不是像在hello.o中那样地址值全为0。这说明重定位工作已完成。同时多出的节是为了能够实现动态链接,如.interp这一节包含动态链接器的路径名,动态链接器通过执行一系列重定位工作完成链接任务。多出来的部分节作用如下:
.gnu.version,.gnu.version_r:与版本有关的信息。
.interp:包含了动态链接器在文件系统中的路径。
.gnu.hash:符号的哈希表,用于加速查找符号。
.dynamic,.dynsym,.dynstr:与动态链接符号相关。
.note.ABI-tag:ELF规范中记录的注释部分,包含一些版本信息。
5.3.3符号表
图5-5 hello的符号表
由于.symtab太长了(有64个条目),所以此处就不粘出完整的图示了。值的注意的是,可执行文件的符号表中多了很多符号,而且额外有一张动态符号表(.dynsym)。printf、puts、atoi、exit、getchar等C标准库函数在动态符号表和符号表中都有表项。此外一个与可重定向目标文件的不同是,这些符号已经确定好了运行时位置。
5.3.4段头表
图5-6 hello的段头表
段头表描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从段头表中可以看到根据可执行目标文件的内容初始化为两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。而在未链接之前,程序头是不存在于elf格式的文件中的。
5.3.5重定位节
图5-6 hello的重定位节
这里的重定位节中的偏移量和加数均有改变,代表着链接的完成,符号名称和类型也发生了一些改变,说明库和hello.o链接成功。
5.4 hello的虚拟地址空间
图5-7 虚拟地址的典型布局
用EDB1.3.0查看内存,发现内存是从0x400000开始的。
图5-8 hello的内存分布
从5.3.1的ELF头中可以看出程序的入口地址为0x401090,对应于节头表中.text节的起始地址。通过edb查看如下:
图5-9 ELF头的内存分布
5.3.2的节头表中的.interp节的起始地址为0x400270。通过edb查看如下:
图5-10 .interp节的内存分布
由上图可以看出0x400270位置处放的正是动态链接器的路径名。
根据5.3.2节头表中的各个节(包括符号表、程序头、重定位节)的位置信息可以找到各个节在内存中的位置。这里就不一一列举了,再举一个例子,比如.rodata节的起始位置为0x402000。通过edb查看如下:
图5-11 .rodata节的内存分布
从图中可以看出printf语句中的格式串%s %s位于.rodata节,属于只读数据。
5.5 链接的重定位过程分析
通过命令objdump -d hello > asm1.txt得到hello文件的反汇编代码。
图5-12 反汇编的生成文件
通过比较hello和hello.o的反汇编代码,可以得到如下不同:
1.hello.o的反汇编代码的地址从0开始,而hello的反汇编代码从0x400000开始。这说明hello.o还未实现重定位的过程,每个符号还没有确定的地址,而hello已经实现了重定位,每个符号都有其确定的地址。
2.hello中除了main函数的汇编代码,还有很多其它函数的汇编代码。如下图所示:
图5-13 asm1.txt中的其他函数
其中_init是程序初始化需要执行的代码,.plt是动态链接的过程链接表。
3.对于跳转,返回指令的地址hello中已经有了明确的数据(PC相对或者是绝对),而hello.o中的地址位置全为0。
图5-14 asm1.txt中的main函数
由此可以看出链接是会为地址不确定的符号分配一个确定的地址,而在该符号的引用处也将地址改为确定值。
hello的重定位过程:
在汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text节中,已初始化数据的重定位条目放在.rel.data中。重定位条目具有如下结构:
图5-15 elf格式文件的节的条目结构---图源CSAPP P469图7-4
其中offset是需要被修改的引用的节偏移。symbol标识被修改引用应该指向的符号。type告知链接器如何修改新的引用。addend是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
ELF中定义了32中不同的重定位类型。其中比较常用的是R_X86_64_PC32和R_X86_64_32。前者重定位一个使用32位PC相对地址的引用,后者重定位一个使用32位绝对地址的引用。
PC相对寻址的重定位代码如下:
图5-16 重定位算法---图源CSAPP P480图7-10
下图为hello.o反汇编代码中的一节,从中可以看出该重定位采用PC相对寻找。
通过查询hello的ELF文件内容得到main函数地址为0x40118c,.rodata的地址为0x402000。将数据带入运算可得:
*refptr = (unsigned)(ADDR(.rodata)+addend - ADDR(main)- Offset)
=(unsigned)(0x402000 + (-4) – 0x40118c-0x1c)
=(unsigned)(0xe64)
将结果与hello的反汇编代码比较,发现结果正确。
图5-18 asm1.txt中的位偏移量
同理可得其他重定位条目。
5.6 hello的执行流程
(1)载入:_dl_start、_dl_init
(2)开始执行:_start、_libc_start_main
(3)执行main:_main、_printf、_exit、_sleep、
_getchar、_dl_runtime_resolve_xsave、_dl_fixup、_dl_lookup_symbol_x
(4)退出:exit
程序名称 | 程序地址 |
ld-2.31.so!_dl_start | 0x7f8e7cc34ee0 |
ld-2.31.so!_dl_init | 0x7f8e7cc486b0 |
hello!_start | 0x401090 |
libc-2.31.so!_libc_start_main | 0x7f9d5d6cffc0 |
libc-2.31.so!_cxa_atexit | 0x7f9d5d6f2e10 |
hello!_libc_csu_init | 0x401210 |
libc-2.31.so!_setjmp | 0x7f9d5d6eec60 |
libc-2.31.so!exit | 0x7f9d5d6f2a70 |
这里采用的是EDB的symbolviewer功能查询对应函数名字来获取其地址的方法,如下图:
图5-19 查询程序及其对应地址
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
通过查看hello的ELF文件如下图:
图5-20 hello1.elf的.got.plt条目
GOT表在调用dl_init之前的内容如下:
图5-21 .got.plt条目在data dump中的内存
在dl_init调用后内容如下图:
图5-22 调用dl_init后.got.plt条目在data dump中的内存
从图中可以看出,在dl_init调用之后,该处的两个8字节的数据都发生了改变。
和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址,GOT[2]是动态链接器ld-linux.so模块中的入口点。
5.8 本章小结
本章主要介绍了链接的概念和作用,详细介绍了hello.o是如何链接生成一个可执行文件的。同时展示了可执行文件中不同节的内容。最后分析了程序是如何实现的动态链接的。链接为程序编写以及版本管理(利用动态链接)提供了一定的便利。程序员不必将所有函数同时写在一个文件中,而是可以分别工作,最后将可重定位目标文件链接在一起;利用静态库,计算机可以利用同一组标准库而不需要占用大量的磁盘空间;通过动态链接共享库,多个进程可以共享一个函数的多个副本而不需要花费多份内存空间,并且可以仅仅通过更新动态链接库而不必重新编译程序来更新版本。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
进程的作用:
进程为用户提供了以下假象:
1.我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
2.处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell-bash的作用:
shell是一种传统的用户界面,本质上也是一个程序。而bash是shell的一种,在1989年发布第一个正式版本,现在许多Linux发行版都把它作为默认shell。它提供了一个界面,用户可以通过这界面访问操作系统内核(是命令行解释器,以用户态方式运行的终端进程)。
Shell-bash的处理流程:
1.终端进程读取用户由键盘输入的命令行;
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量;
3.检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令;
4.如果不是内部命令,调用fork( )创建新进程/子进程;
5.在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait)等待作业终止后返回;
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
当在shell上输入./hello命令时,命令行会首先判断该命令是否为内置命令,如果是内置命令则立即对其进行解释。否则将其看成一个可执行目标文件,再调用fork创建一个新进程并在其中执行。
当shell运行一个程序时,父进程通过fork函数生成这个程序的进程。新创建的子进程几乎但不完全与父进程相同,包括代码、数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于他们有不同的PID。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。
6.4 Hello的execve过程
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。加载并运行需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构;
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零;
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域;
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
图6-1 加载器是如何映射用户地址空间的区域的
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
进程时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户态和内核态:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
上下文切换:当一个进程正在执行时,内核调度了另一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。在进行上下文切换时,需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。
hello的进程执行过程如下:
hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程以加载新的进程进行执行。同时将hello进程从运行队列中移入到等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程并执行,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
图6-2 进程的上下文切换
当hello调用getchar的时候,实际落脚到执行输入流是stdin的系统调用read,hello之前运行在用户模式,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。
6.6 hello的异常与信号处理
异常分为以下几类:
图6-3 异常种类
在hello程序执行过程中这几类异常都可能出现。当出现异常时,操作系统会根据异常表进行一个间接过程调用,找到异常对应的异常处理程序。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下3种情况中的一种:
1.处理程序将控制返回给当前指令,即当事件发生时正在执行的指令;
2.处理程序将控制返回给当前指令的下一条指令,即如果没有异常发生将会执行的下一条指令;
3.处理程序终止被中断的程序。
下面详细介绍hello执行时各种异常出现的情况:
6.6.1不停乱按(包括回车)
如果乱按过程中没有按回车,则只会在屏幕上显示输入的内容。如果输入回车,则getchar读回车,并把回车前的字符串当作shell输入的命令。
图6-4 运行后乱按并且回车
6.6.2 Ctrl-Z
Ctrl-Z后输入ps,jobs等命令仍会正常工作,同时可以看出此时hello程序的状态为Stopped。fg的功能是使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行。
图6-5 运行后键入Ctrl-Z
6.6.3 Ctrl-C
如果在程序运行过程中输入Ctrl+C,会让内核发送一个SIGINT信号给到前台进程组中的每个进程,结果是终止前台进程。
图6-6 运行后键入Ctrl-C
6.6.4 Ctrl-Z后运行ps\jobs\pstree\fg\kill等命令
Ctrl-Z后输入ps,jobs等命令仍会正常工作,同时可以看出此时hello程序的状态为Stopped。fg的功能是使第一个后台作业变为前台,而第一个后台作业是hello,所以输入fg 后hello程序又在前台开始运行。
图6-7 运行后键入Ctrl-Z,然后输入ps、jobs、fg指令
6.7本章小结
本章介绍了进程的概念和作用,同时介绍了Shell的一般处理过程和作用。分析了fork和execve函数的功能,展示了hello进程的执行以及hello的异常和信号处理。进程是一个执行中程序的实例(Instance),它是计算机科学中最深刻、最成功的概念之一。即使操作系统中同时有多个程序执行,我们看到的也像是操作系统仅在运行前台程序一样,这是通过上下文切换实现的。操作系统根据某种特定的策略调度进程(在不同操作系统中可能是不同的,这也是为什么我们不能假定父子进程的执行先后)来在不同进程间快速地交错执行。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:
程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
线性地址:
也叫虚拟地址,和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。
虚拟地址:
同线性地址。在IA 32中,线性地址由逻辑地址经过转换得到。先用段选择符到全局描述符表(GDT)中取得段基址,再加上段内偏移量,即得到线性地址。IA 64中,由于不存在段,偏移量也就不需要转换才能得到线性地址了,因此虚拟地址同线性地址。
物理地址:
用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,表示具体的是代码段寄存器还是栈段寄存器抑或是数据段寄存器。
索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。
Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。
7.3 Hello的线性地址到物理地址的变换-页式管理
系统将虚拟页作为进行数据传输的单元。Linux下每个虚拟页大小为4KB。物理内存也被分割为物理页, MMU(内存管理单元)负责地址翻译,MMU使用页表将虚拟页到物理页的映射,即虚拟地址到物理地址的映射。
图7-1 线性地址到物理地址的变换
7.4 TLB与四级页表支持下的VA到PA的变换
每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
图7-2 VA到PA的变换
多级页表:
将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。VPN被分为k个部分,第一级VPN结合基址寄存器得到一个页表条目,其中存放下一级页表的基址,再结合VPN2,得到第三级页表基址,继续寻找,以此类推,直到最后确定对应的物理页号,与VPO结合,得到由PPN与PPO结合成的物理地址,用于物理地址寻址。
图7-3 多级页表
多级页表的使用从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在。第二,只有一级页表才需要总是在主存中。虚拟内存系统可以在需要时创建、页面调入或调出二级页表。
7.5 三级Cache支持下的物理内存访问
在从TLB或者页表中得到物理地址后,根据物理地址从cache中寻找。到了L1里面以后,寻找物理地址要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入cache中,其中可能会发生块的替换等其它操作。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。
7.6 hello进程fork时的内存映射
当 fork 函数被 shell 进程调用时,内核为新进程创建各种数据结构,并分配给 它一个唯一的 PID,为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
shell在fork子进程后要立即调用execve加载并运行hello程序。这需要以下几个步骤:
1.删除已经存在的用户区域(即除了内核以外的部分,包括代码、数据、bss、堆、栈、共享库内存映射区域);
2.映射私有区域:为了用新进程替代原有进程,execve需要为新程序的用户区域创建新的区域结构,且它们均为私有写时复制的。其中代码和数据区域被映射为hello中的.text和.data段,bss区域是请求二进制零的,其长度包含在hello文件中;栈和堆也是请求二进制零的,其初始长度均为0;
3.映射共享区域:如果一个程序与共享对象(目标)链接,(在hello中libc.so就是其中一个),那么这些对象需要被映射到虚拟地址中的共享区域内。
4.设置PC。对于hello进程,execve设置rip寄存器到代码区域的入口点,程序可以开始执行了。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
缺页中断处理:通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。
图7-4 缺页错误时的中断处理
7.9动态存储分配管理
动态内存分配器维护者一个进程的虚拟内存区域,称为堆。(如图7.9.1所示),分配器将堆视为一组不同的大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都是要求显示的释放分配块。
显式分配器:要求应用显示的释放任何已分配的块。例如C标准库提供一个叫做malloc程序包的显示分配器。
隐式分配器:要求分配器检测一个已分配块何时不再被程序使用,那么就释放这个块。隐式分配器也叫垃圾收集器。
隐式空闲链表:
一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。
图7-5 一个简单的堆块的格式
具体的隐式空闲链表形式如下:
图7-6 一个隐式空闲链表
放置已分配的块当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。分配器搜索方式的常见策略是首次适配、下一次适配和最佳适配。
当分配器找到一个匹配的空闲块时,通常将空闲块分割为两部分。第一部分变为了已分配块,第二部分变为了空闲块。
当分配器找不到合适的空闲块一个选择是合并那些在物理内存上相邻的空闲块,如果这样还不能生成一个足够大的块,分配器会调用sbrk函数,向内核请求额外的内存。
合并空闲块合并的情况一共分为四种:前空后不空,前不空后空,前后都空,前后都不空。为了提高合并效率,Knuth提出了一种采用边界标记的技术快速完成空闲块的合并。其所用到的结构如下图所示:
图7-7 使用边界标记的堆块的格式
显示空闲链表:
显示空闲链表是将空闲块组织为某种形式的显示数据结构。在每个空闲块中,都包含一个前驱和后继的指针。
使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。维护链表可以采用后进先出(LIFO)的顺序或者按照地址增大的顺序来维护。
7.10本章小结
本章主要介绍了有关内存管理的知识。详细阐述了hello程序是如何存储,如何经过地址翻译得到最终的物理地址。介绍了hello的四级页表的虚拟地址空间到物理地址的转换。阐述了三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。存储管理是操作系统中软件与硬件结合的典型示例。虚拟内存提供了比可用内存更大的地址空间,为进程之间独立运行提供了可能,并保障了内存的安全(我们写出的产生Segmentation Fault的程序就是虚拟内存完美工作的最好证明)。高速缓存加速了程序的运行,使程序员可以从更多的方面来优化他们的程序。存储管理也是C/C++比其他语言更自由之处(在出错时也更让人摸不着头脑),需要我们深入学习来写出更加安全高效的程序。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2.Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3.改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
4.读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。
5.关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。
Unix I/O函数:
1.int open(char* filename,int flags,mode_t mode) ,进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。 open函数将filename 转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在 进程中当前没有打开的最小描述符,flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。
2.int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。
3.ssize_t read(int fd,void *buf,size_t n),read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。返回值-1 表示一个错误,0 表示 EOF,否则返回值表示的是实际传送的字节数量。
4.ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
8.3 printf的实现分析
printf的函数体如下:
图8-1 printf函数体
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。printf用了两个外部函数,一个是vsprintf,还有一个是write。
vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。write函数将buf中的i个元素写到终端。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar的函数体如下:
图8-2 getchar函数体
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf和getchar函数的实现。Linux的文件系统将许多概念都抽象成文件:网络、磁盘或是终端。这种抽象提供了一种一致的对不同设备的处理方式。在调用Linux提供的接口时,我们应当注意如read和write函数返回的不足值,对程序做出正确的处理。
结论
1.编写,将代码键入hello.c;
2.预处理,将hello.c调用的所有外部的库展开合并到一个hello.i文件中;
3.编译,将hello.i编译成为汇编文件hello.s;
4.汇编,将hello.s会变成为可重定位目标文件hello.o;
5.链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello;
6.运行:在shell中输入./hello 120L020415 杜鑫成 1;
7.创建子进程:shell进程调用fork为其创建子进程;
8.运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数;
9.执行指令:CPU为其分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流;
10.访问内存:MMU将程序使用的虚拟内存地址通过页表映射成物理地址;
11.动态申请内存:printf调用malloc向动态内存分配器申请堆中的内存;
12.信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起;
13. 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
附件
文件名 | 文件的作用 |
hello.c | 源程序文件(文本) |
hello.i | hello.c经过cpp预处理后生成的程序(文本) |
hello.s | hello.i经过cc1编译后产生的汇编程序(文本) |
hello.o | hello.s经过as汇编后产生的可重定位目标程序(二进制) |
hello | hello.o和其他可重定位目标程序以及一些必要的系统目标文件通过ld链接起来(二进制) |
hello.elf | hello.o文件的elf格式文件(文本) |
hello1.elf | hello文件的elf格式文件(文本) |
asm.txt | hello.o的反汇编代码文件(文本) |
asm1.txt | hello的反汇编代码文件(文本) |
参考文献
[1] Randal E.Bryant. 深入理解计算机系统. 北京:机械工业出版社,2019:6-1.
[2] 王爽. 汇编语言(第三版). 北京:清华大学出版社,2016:1-9.
[3] https://blog.csdn.net/qq_32812243/article/details/50590104
[4] https://blog.csdn.net/wyn1564464568/article/details/124780447
[5] https://blog.csdn.net/A15249/article/details/118299596
[6] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
[7] https://blog.csdn.net/qq_44783613/article/details/120570702
[8] https://blog.csdn.net/zwl1584671413/article/details/108146790