计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L020614
班 级 011
学 生 刘昕烨
指 导 教 师 郑贵滨
计算机科学与技术学院
2021年5月
摘 要
无论对于哪个程序员来说,Hello World都是一个重要的开始。本论文利用gcc、edb,bash等工具,通过CSAPP书中所学知识以及网上查找到的相关资料研究了hello程序在Linux系统下的从预处理阶段开始到编译,汇编,链接,执行,直到其被回收的整个生命周期与历程,以及其中涉及到的从信息的表示,到进程的管理,再到内存管理的一系列操作系统与硬件为其提供的运行媒介。从而融合在CSAPP书中所学的知识,完整的了解了hello程序在底层是如何走完它那虽然短暂但精彩的一生的。
**关键词:**计算机系统;程序的一生;进程;IO;异常;
**
**
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 11 -
7.7 hello进程execve时的内存映射 - 11 -
第1章 概述
1.1 Hello简介
- Form Program to Process
首先我们需要利用vim等编辑器编写程序hello.c,即program。然后这个文件进行一系列的预处理,编译,汇编,链接等过程生成一个.out的可执行目标程序。
接着我们打开shell之后输入该目标程序的地址,那么shell就会利用fork函数为hello创建一个新的子进程,即progres。然后在该子进程中调用execve函数,将hello的可执行文件进行加载,运行。
- From Zero to Zero
首先在真正加载前shell会为hello申请虚拟内存空间。然后MMU将物理内存与虚拟内存进行映射。当需要物理地址来读取数据时,CPU会向MMU发出一个虚拟地址,MMU将虚拟地址转化为物理地址之后,利用DMA数据不通过处理器从磁盘到达主存。
当hello的代码与数据被加载到主存后处理器开始执行main中的机器语言指令,将字符串“hello,world\n”中的字节从主存复制到寄存器文件再复制到现实设备最终显示在屏幕上。
同时在运行程序时处理器内核还需要为hello分配时间片(及其确定的一段运行时间),使得其看似独享整个资源。
当我们在程序运行期间,输入信号时,会产生中断。如当按下ctrl z 或 ctrl c时,内核会向hello进程发送一个信号让其暂停或者终止。
子进程终止之后,父进程将变成僵尸进程的子进程进行回收,避免占用资源。
1.2 环境与工具
硬件:AMD Ryzen 7 Mobile 4800U; 16G RAM; 256GB SSD
软件: Ubuntu 20.10
工具: VIM , GCC, EDB, OBJDUMP, READELF, LD
1.3 中间结果
hello.c——原文件
hello.i——预处理之后文本文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标执行
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf格式,用来看hello.o的各节信息
hello.dump——hello.o的反汇编文件,用来看汇编器翻译后的汇编代码
hello.objdump——hello的反汇编文件,用来看链接器链接后的汇编代码
1.4 本章小结
本章节简述了 P2P 和 020 的含义,漫游了一下hello的一生,解释了hello从运行到被回收的过程经历的各个阶段。列出了测试环境和工具以及中间结果的文件名和文件作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
C程序中的源代码中包含以#开头的各种编译指令,这些指令称为预处理指令。预处理指令不属于C语言的语法,但在一定意义上可以说预处理扩展了 C。ANSI C 定义的预处理指令主要包括:文件包含、宏定义、条件编译和特殊控制等4类。
C预处理器(C Pre-Processor)也常简写为 CPP,是一个与C编译器独立的小程序,预处理器并不理解C语言语法,它仅是在程序源文件被编译之前,实现文本替换的功能。预处理器根据以字符#开头的命令修改原始的C程序。 比如 hello.c 中的命令告诉预处理器读取对应的三个系统头文件的内容,并把它直接插入到程序文本中,结果就得到了另一个C程序。
作用[1]:
#include 指令告诉预处理器(cpp)读取源程序所引用的系统源文件,并把源文件直接插入程序文本中。
执行宏替换。将目标的字符替换为我们所定义的字符。包括包括定义宏 #define和宏删除 #undef。例如:#define PI 3.1416定义符号常量 PI,#undef PI 删除前面该宏的定义
条件编译。根据定于的条件,来确定编译的条件,即目标是否是真正需要的,类似于if else。
特殊符号,预编译程序可以识别一些特殊的符号,预编译程序对于在源程序中出现的这些串用合适的值进行替换,如#error:使预处理器输出指定的错误信息,通常用于调试程序。
2.2在Ubuntu下预处理的命令
图2-1
2.3 Hello的预处理结果解析
使用Vim打开hello.i,发现原来的hello.c已经被拓展成了3060行,前面的代码是hello.c的三个#include指令包含的头文件展开后的代码,先寻找我们原来的main函数,main函数从第3047行开始,如下图。
图2-2
此时再回过头来看对之前的头文件的处理,以对源文件第一条#include指令的处理为例。
cpp到默认的环境变量下搜索stdio.h头文件,打开/usr/include/stdio.h这一目录,然后发现其中还有#include指令,于是再去搜索这个新的文件中包含的头文件,直到最后的文件中没有#include指令。
在这个过程中把所有文件中的#define之类的宏指令进行处理(执行宏替换并且通过条件判断确定是否要处理定义的指令)。如图是对stdio.h包含文件的展开。
图2-3
2.4 本章小结
本章主要介绍了预处理的概念及作用,然后在Ubuntu上生成了对应的.i文件。并结合处理后的hello.i文件对cpp的处理过程进行了分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
编译程序:将高级语言(源语言)翻译成汇编语言或机器语言之类的低级语言(目标程序)的翻译程序。
编译可以大致分为五个阶段[2],它们分别是:词法分析,语法分析,语义检查,中间代码生成,目标代码生成。
- 词法分析:对构成源程序的字符串从左到右进行扫描和分解,根据语言的词法规则,识别出一个一个具有独立意义的单词。(词法规则:单词的形成规则,规定了哪些字符串构成一个单词符号。)
- 语法分析:根据语言的语法规则从单词符号串中识别出各种语法单位并进行语法检查。
- 语义分析及中间代码生成:首先对每一种语法单位进行静态的语义审查,然后分析其含义,并用中间代码或目标语言来描述这种语义。
- 代码优化:对前阶段产生的中间代码进行等价变换或改造,主要包括局部优化和循环优化等。
- 目标代码生成:将中间代码变换成特定机器上的绝对指令代码、可重定位的指令代码或汇编指令代码。
作用:编译最终生成可以在机器上执行的代码,为不同高级语言提供了相同的输出语言。为最终在机器上执行做准备。
3.2 在Ubuntu下编译的命令
图3-1
3.3 Hello的编译结果解析
3.3.1常量
hello.c中出现了字符串常量以及整型常量0,8,1,2,3,4。
- 整型常量的值保存在.text节中作为指令的一部分。
例如在原汇编代码中有这一句:
- if(argc!=4)
翻译为汇编代码后变为了:
图3-2
我们可以看出%rdi中存放的是argc的值,接着将其放于-20(%rbp)中与立即数4相比较,因此我们看出这些常量是存放在.text节的数据本身中的。
- 字符串常量的值保存于.rodata节
图3-3
如图3-3里的第一行的字符串就对应了源代码中
- printf(“用法: Hello 学号 姓名 秒数!\n”);
的printf语句中的字符串。其中由于在UTF-8编码中三个字节对应一个中文汉字因此最终对应的是如图中的编码。
3.3.2变量
全局变量在本程序中没有出现,它储存在.data节,初始化不需要汇编语句,而是直接完成的。
局部变量存储于栈中或者寄存器中。接下来以局部变量i为例,其在源代码中有以下几行:
- int i;
- for(i=0;i<8;i++)
对应于汇编代码中及:
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
- .L3:
- cmpl $7, -4(%rbp)
- jle .L4
以上几行与之对应那么我们可以确定i这一局部变量是存在于-4(%rbp)中的。
3.3.3赋值
赋值操作一般通过汇编语句操作movq或者leaq操作来实现。例如for循环开始时对i的赋值。
- movq -32(%rbp), %rax
3.3.4算数操作
可以通过汇编指令自带的算术操作来实现。如将i的值加一后赋给i可以通过以下指令实现:
- addl $1, -4(%rbp)
其相等于i+=1;
3.3.5关系操作与控制转移
控制转移操作一般通过cmpl与jmp或者jne等指令的联合使用来实现,而关系操作可以通过识别某些算术操作设置的条件码来看出。
接下来分析hello程序中涉及到的这方面的代码:
- if(argc!=4){
- printf(“用法: Hello 学号 姓名 秒数!\n”);
- exit(1);
- }
这是一个if语句其中还有判断符 !=
图3-4
.s文件中通过将存在栈中的字符i与4进行比较如果,相等及ZF为1则跳过一段语句,否则执行printf操作并退出从而实现了比较以及控制转移。
for循环与之类似开头时执行赋值操作然后进行一些比较,把这些比较代码提取出来:
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
- .L4:
- body of the loop
- .L3:
- cmpl $7, -4(%rbp)
- jle .L4
如上代码所示。其与if语句比较后跳转并无二质只是一个往以前代码跳一个向之后代码跳而已。
3.3.6数组/指针/结构操作
这一部分对应于hello程序中main函数的第二个参数指针数组及int main(int argc,char *argv[]) 在这一数组中argv[0]指向的是输入程序的路径与名称,接着后面出现的三个
参数是要求输入的学号姓名与秒数。在.s程序中这一结构是通过相对偏移量来实现的。我们以以下这句c语言语句的实现来解析。
printf(“Hello %s %s\n”,argv[1],argv[2]);
图3-5
图3-5中前7句完成了输入参数argv[1],argv[2]的赋值首先第一句把argv的值赋给了rax,再利用加16的方法让rax指向了数组中的第3个元素argv[2],接着把其值赋给了rdx完成了参数的赋值。
从中我们可以看出对数组元素的使用首先要确定指向第一个元素的指针在此例中为argv,将其赋给某个寄存器在此例子中为rax,接着对其进行加减8的倍数来指向数组中任意元素。
3.3.7函数操作
首先需要注意的是X86-64中,过程调用中1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
接着我们分析一下main函数开始时的行为。
main函数参数传递:argc与argv
函数的局部变量:函数在运行时首先给main函数分配其对应的栈帧接着把对应的局部变量存入,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KS93ohh8-1652980513694)(D:\哈工大专业课\计算机系统\实验\2022\大作业\大作业2022\media\53bf64012305c0d414701d667306e4b9.png)]
图3-6
开始的一个指令将rbp的值保存然后其中倒数两条指令将作为参数传入main函数的argc与argv保存起来。
由于整个函数的结构,我们分别分析hello内部函数的调用过程,main函数的返回过程的解释放于最后:
-
printf函数
源代码中有两个对printf函数的调用,我们选取其中一个进行解析:
参数传递:argv[1],argv[2]
函数调用:for循环中被调用
函数返回:放于rax中
printf(“Hello %s %s\n”,argv[1],argv[2]);
其对应的汇编代码为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GpYj6cbf-1652980513694)(D:\哈工大专业课\计算机系统\实验\2022\大作业\大作业2022\media\8f3c23736a922788092759f9713183b9.png)]
图3-7
这一段代码中首先分别将argv[1],argv[2]分别赋给rsi与rdx作为参数,接着将"Hello %s %s\n"这一段字符串的地址赋给rdi,然后利用call调用printf函数。
函数返回值放于rax中
-
sleep函数与atoi函数
sleep(atoi(argv[3])
参数传递:sleep参数为atoi的返回值,atoi的参数为argv[3]
函数调用:在for循环中被调用
函数返回:放于rax中
这一部分对应的汇编代码如下图
图3-8
首先与上一部分相同,我们先把argv[3]的值赋给rdi,然后调用atoi函数,接着把存储在rax中的返回值赋给rdi接着利用call函数调用sleep函数。
- getchar函数
参数传递:无
函数调用:在main函数尾被调用
函数返回:放于rax中
由于没有输入参数,直接用call函数加地址的形式调用即可。
- exit函数
参数传递:1(传入方式将1赋给rdi)
函数调用:argc!=4时
对应的汇编代码:
图3-9
main函数的函数返回:main函数最后将0放于rax中返回,这里需要注意的是返回前有一个leave指令这是用来关闭栈帧的函数,具体在这个函数中相当于mov %rbp,%rsp ,pop %rbp然后接着用ret指令释放栈空间退出函数即可。
3.4 本章小结
本章大概介绍了编译的概念与作用,介绍了linux下的编译指令,通过hello示例表现了c程序如何转化为汇编代码,并且对生成的编译结果中的包括变量,常量,赋值,算数,关系与控制,数组指针以及结构,函数是如何实现的做了解释,经过该步骤生成的 hello.s已经是更加接近机器层面的汇编代码。
认识到了编译程序所做的工作,就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码表示。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:运行汇编器(as)将汇编语言(在本报告中为hello.s)翻译成机器能读懂的机器语言指令,并将这些指令打包成可重定位目标程序hello.o(hello.o是一个二进制文件也是可重定位目标文件)。这个过程叫汇编。
汇编的作用:产生机器能读懂的代码(机器码),使得程序能被机器执行。同时使得程序员可以在更高抽象层面编写代码,不必担心如何书写抽象的机器码。
4.2 在Ubuntu下汇编的命令
图4-1
可重定位目标elf格式
4.3.1 Ubuntu下的readelf命令
图4-2
4.3.2ELF头
ELF头包括包含了系统信息,编码方式,ELF头大小,节的大小和数量等等一系列信息。
图4-3
比如第一行Magic节[3]包含了系统信息,编码方式,ELF头大小,节的大小和数量等等一系列信息,其第五个字节表示是32位还是64位,第六个字节表示其是小端序还是大端序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EbnoIJ0c-1652980513697)(https://s2.loli.net/2022/05/20/4GSFDjUkLVnedRP.png)]
图4-4
ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括机器类型(AMD X86-64)、目标文件的类型(REL)、ELF头的大小(64bytes)、节头部表中条目的大小和数量、字符串表段头索引(Section header string table index)(如这个文件中表示字符串表段头在段表中的偏移为13)等信息。
4.3.3节头部表
正如其在hello.elf文件中标注的那样,这一部分描述了各个节的信息,包括其名字,类型,地址,偏移量,大小,访问权限等信息。
图4-5
4.3.4重定位节
.rela.txt节包含了.text节中需要重定位的信息,告诉链接器在将目标文件合成可执行文件时如何修改这个引用。具体如下图:
图4-6
这8个条目分别代表了8个需要修改引用的地方,具体为源程序中:printf函数字符串常量"用法: Hello 学号 姓名 秒数!\n",puts函数,exit函数,第二个printf的字符串常量"Hello %s %s\n",argv[1],argv[2],printf函数,atoi函数,sleep函数,getchar函数。它们分别含有该符号相对.text节开头的偏移量,重定位条目类型,名字等值。
由于它们的作用是相似的,以第一个条目为基准解释其重定位原理。首先由于其类型为R_X86_64_PC32说明其对应的是重定位一个使用32位PC相对地址的引用。假设这一条目为r,那么我们可以从这个条目中获取信息:
计算该重定位目标地址的算法如下:
(想要计算该地址还需要假设几个量,先假设该字符串的实际地址ADDR(r.symbol)为ADDR(act)而且.text节的地址假设已经知道为ADDR(text),那么地址*refptr的计算方法为:)
这样当实际分配内存后假设的量的大小就变成了已知量,我们就可以按照这个方法将原来.text节的refaddr处的地址修改为计算之后的重定位目标地址即可完成重定位。
4.3.5符号表
符号表包含了此模块定义以及引用的符号的信息,可以用来定位程序中的符号。这张表中还包含有相对对应节的偏移量,是局部符号还是全局符号。
图4-7
4.4 Hello.o的结果解析
机器语言就是一串01序列组成的串。
在确定开头的情况下机器语言可以被按照开始为指令之后为操作码递归的解读。由于具体的每个操作码对应的操作数长度是固定的,因此只要开头确定那么汇编语言与机器语言的语句是可以一一对应的,这也正是汇编器做的工作。
但是它们二者仍然在某些细节上的表示是不同的,介绍如下:
4.4.1 Ubuntu下的objdump命令
图4-8
4.4.2分支转移指令
这里以hello程序中的第一个判断为例,给出表格如下。
我们可以看到对于.s文件跳转其指令是跳转到.L2这样的段名称,但是显然在机器码中是不可能这样实现的。因此在机器码中其跳转指令中的值是确定的地址值。
机器语言 | 汇编语言 |
---|---|
表4-1
4.4.3函数调用
同样的我们把调用的第一个函数printf的代码对比放在下面的表中。我们可以发现对于.s文件它调用函数的方法是直接利用call加函数名,同样的这样的方法也无法使用于机器语言。真正的代码时在call指令对应得字节后加对应函数的地址。但是我们看右边的代码可以发现它的地址为全0也即下一条指令。这是因为hello.c中调用了共享库中的函数,最终在链接时才会更改,因此此时将地址设置为全0,然后将其加入.rela.text节中等待下一步链接。
机器语言 | 汇编语言 |
---|---|
表4-2
4.4.4对字符串常量的访问
这里以第一个printf字符串中字符串常量的访问为例,我们看到机器语言中的处理方式与上一步函数时一样的,等到连接时再把具体的值赋给.text节的代码,先把这个字符串常量的地址加入.rela.text节中等待下一步链接。而.s文件中的调用方式为段名称再加%rip。
机器语言 | 汇编语言 |
---|---|
表4-3
4.5 本章小结
本章介绍了汇编的过程,并且查看了生成.o文件的elf头的各个部分的组成与内容,比较了生成的机器码与对应得汇编代码的不同以及映射关系。到了这一步hello程序已经变为了可以被机器识别的二进制码,马上就可以运行了,而这一切就是汇编器的功劳。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接某种程度上说是一种打包的过程,即将各种代码和数据片段收集并组合成为一个单一文件的过程,并且这个文件可被加载到内存并执行。
链接的作用:链接使得分离编译(seperate compila)成为可能同时使我们更加容易维护程序。由于链接器可以把小模块合成大模块,因此我们可以独立的修改和编译需要修改的小的模块,然后简单地重新编译它,并重新链接应用即可,无需直接重新编写大模块。
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
图5-1
5.3 可执行目标文件hello的格式
使用命令readelf -a -W hello来读取hello的ELF头。
其中的Section Headers部分截图如下,这个部分记录了各个段的基本信息。从左到右大概为:编号,名称,类型,起始地址,在程序中的偏移量,大小,以及标志其权限的一些信息。
图5-2
5.4 hello的虚拟地址空间
打开edb查看程序hello,发现默认载入的地址为0x400000~0x400fff。
图5-3
在这个节中的数据与之前的节头表对应如下:
图5-6
因此我们可以看出这里的节与节头表中的声明是一一对应的。
我们只验证.rodata节的内容,其余节类似。首先在datadump选项中找到对应的地址区域,找到.rodata节的起始地址,然后我们可以看到其中的内容含有hello.c中的两个字符串。
图5-7
经过验证在图5-2中的其他节也可以通过对应的Address在DataDump中找到这里就不一一列举了。
5.5 链接的重定位过程分析
使用命令objdump -d -r hello > hello.objdump 生成hello的反汇编文件,将其与hello.dump(对hello.o的可重定位文件进行的反汇编)进行对比。
图5-8
我们可以发现对比两者的.text节,hello程序并不是直接从main函数开始了,而是增加了两个部分start,_dl_relocate_static_pie,说明程序开始时并不是从main开始的而是要利用别的模块。
.text节中主要的汇编代码区别不大,主要是将完成了重定位的过程,将相对寻址变为了绝对寻址:链接器链接器把hello.o文件中的地址(偏移量)加上程序在虚拟内存中的起始地址0x400000与.text节的偏移量之和就得到了hello.objdump中的地址。对于具体的指令来说,控制转移即jmp指令后的地址由偏移量变为了偏移量+具体的函数的起始地址,对于函数调用call后面的地址由链接器执行重定位后直接计算出实际地址,下面具体解释:
对于函数调用来说,以hello中的第一个puts函数为例,链接器解析重定条目时发现对该外部函数调用的类型为R_X86_64_PLT32的重定位(4.3.4中的重定位条目)。此时动态链接库中的函数已经加入到了PLT节中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,来指向对应函数。在这里我们计算得到的理论值为:
0x401090-0x40114a = -0xba 转换为二进制补码再转换为16进制再转换为小端序为:46 ff ff ff正好与源代码相同。
对全局变量引用来说,我以4.3.4节介绍的字符串符号为例介绍,当时以及介绍过重定位的过程就不再赘述。接下来简要叙述验证过程,先需要知道具体的字符串的位置ADDR(act)= 0x402008与.text节的地址ADDR(text) = 0x4010f0接着带入公式知道相对地址为:0x402008 - 0x4 - 0x401125 – 0x1c = 0xec3与hello.objdump中相同。
此外hello.objdump中增加了许多外部链接的共享库函数。如puts@plt共享库函数,printf@plt共享库函数以及getchar@plt函数。
图5-9
同时,hello.objdump比hello.dump多出了几个节:.init节、.plt节、.fini节。例如:经查询,.init节是程序初始化需要执行的代码,.fini节是程序正常终止时需要执行的代码,.plt节是动态链接中的过程链接表。
图5-10
5.6 hello的执行流程
子程序名 | 地址 |
---|---|
ld-2.27.so!_dl_start | 0x7fce8cc38ea0 |
ld-2.27.so!_dl_init | 0x7fce8cc47630 |
hello!_start | 0x400500 |
libc-2.27.so!__libc_start_main | 0x7fce8c867ab0 |
libc-2.27.so!__cxa_atexit | 0x7fce8c889430 |
libc-2.27.so!__libc_csu_init | 0x4005c0 |
hello!_init | 0x400488 |
libc-2.27.so!_setjmp | 0x7fce8c884c10 |
-libc-2.27.so!_sigsetjmp | 0x7fce8c884b70 |
–libc-2.27.so!__sigjmp_save | 0x7fce8c884bd0 |
hello!main | 0x400532 |
hello!puts@plt | 0x4004b0 |
hello!exit@plt | 0x4004e0 |
*hello!printf@plt | |
*hello!sleep@plt | |
*hello!getchar@plt | |
ld-2.27.so!_dl_runtime_resolve_xsave | 0x7fce8cc4e680 |
-ld-2.27.so!_dl_fixup | 0x7fce8cc46df0 |
–ld-2.27.so!_dl_lookup_symbol_x | 0x7fce8cc420b0 |
libc-2.27.so!exit | 0x7fce8c889128 |
5.7 Hello的动态链接分析
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用PLT(过程链接表)+ GOT(全局偏移量表)实现函数的动态链接。GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。第一次执行时,为 GOT赋上相应的偏移量,初始化了函数调用,dl_init就是做了这件事。此后每次执行时不需要经过如此操作,每次都直接跳转到目标函数的地址。
由图5-2可知,.got.plt的地址是0x404000,其内容如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7siyiS0r-1652980513703)(https://s2.loli.net/2022/05/20/3LS5htrUf7Ni1RX.png)]
图5-11
如下图,可以看到调用dl_init后0x404008和0x404010处的两个8字节的数据发生改变,出现了两个地址0x7f85442c2190和0x7f85442ad200。这就是GOT[1]和GOT[2]。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pCSsSM64-1652980513704)(https://s2.loli.net/2022/05/20/H4nNavFDyBfr9IJ.png)]
图5-12
如下图红线部分,其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址。
图5-13
GOT[2]指向的目标程序是动态链接器ld-linux.so运行时地址。
图5-14
5.8 本章小结
本章介绍了链接的概念和作用,对链接后生成的可执行文件hello的elf格式文件进行了分析,分析了hello的虚拟地址空间、重定位过程、执行过程,以及动态链接的各种处理操作。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是程序的一个实例,系统的每个程序都运行在某个程序的上下文中。每一个进程都有它自己的地址空间,并且在该地址空间内,每个进程的地址空间结构的一样的,这方便了编译器的编译。
作用:进程为用户提供了如下假象:程序好像在独占处理器、内存,并且处理器无间断地运行进程中的指令,从而使得该进程好像是系统中唯一运行的程序。
6.2 简述壳Shell-bash的作用与处理流程
shell-bash的作用[4]: shell-bash是一个程序,它可以读取用户输入的命令,交互性地解释并代表用户执行进程。它也能够通过调用系统级的函数或功能执行程序、建立文件、进行并行操作等。同时它也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行。
shell-bash的处理流程[4]:
1. 终端进程读取用户由键盘输入的命令行,分析命令行字符串,获取命令行参数,并传递给execve的argv向量
2. 检查argv中的第一个命令行参数是否是一个内置的shell命令,若是内置命令则直接执行
3. 如果不是内部命令,调用fork()创建子进程,在子进程中,用argv中的参数,调用execve()执行指定程序
4. 在父进程中如果用户没要求后台运行(命令末尾没有&号)则使用waitpid等待作业终止后返回并回收,如果用户要求后台运行(如果命令末尾有&号),则shell直接返回
6.3 Hello的fork进程创建过程
首先,打开Terminal输入:./hello 120L020614 刘昕烨 1
接下来shell会分析这条命令,由于./hello不是一条内置的命令,于是判断./hello的语义是执行当前目录下的可执行目标文件hello。然后Terminal会调用fork函数创建一个新的运行的子进程。
子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本。这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间的区别在于它们拥有不同的PID。
内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发运行而独立的。
在子进程执行期间,父进程默认选项是显示等待子进程的完成。父进程和子进程独立运行,二者结束顺序不可知。父进程负责回收子进程。由于fork的返回值不同,所以可以根据fork的返回值不同来区分子进程和父进程。
6.4 Hello的execve过程
在fork之后,子进程调用execve函数,execve函数加载并运行可执行目标文件filename,且使用参数列表argv和环境变量列表envp,在新创建的子进程的上下文中加载并运行hello程序。execve函数调用一次且从不返回,除非发生错误时。execve 函数将用目标程序的进程替换当前进程,并传入相应的参数和环境变量,控制转移到新程序的 main 函数。
图6-1
加载并运行hello需要以下几个步骤:
1. 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2. 映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区被映射为hello文件中的.text和.data区。栈和堆区域也是请求二进制零的,初始长度为零。
3. 映射共享区域。如果hello程序与共享对象链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置PC。设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。下一次调度这个进程时,它将从这个入口点开始执行。
6.5 Hello的进程执行
先解释一些基本概念:
上下文:系统中的每个程序都运行在某个进程的上下文中。上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
用户模式与内核模式:处理器通常用某个控制寄存器的一个模式位来提供用户模式和内核模式的功能。设置了模式位时,进程就运行在内核模式中,该进程可以执行指令集中的任何指令,可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中,用户模式中的进程不允许执行特权指令。
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。上下文切换流程是:1.保存当前进程的上下文。2.恢复某个先前被抢占的进程被保存的上下文。3.将控制传递给这个新恢复的进程。
然后分析hello的进程调度:
hello在刚开始运行时内核为其保存一个上下文,同时并发的运行其他进程,进程在用户模式下运行,当没有异常或中断信号的产生,hello将一直正常地执行,而当出现异常或系统中断时,内核将启用调度器休眠当前进程,并在内核模式中完成上下文切换,将控制传递给其他进程。
例如当程序在执行sleep函数时,系统调用主动地请求让调用进程休眠,调度器抢占当前进程,并发生上下文切换,内核将控制转移到其他进程,将 hello 进程从运行队列加入等待队列,从用户模式变成内核模式,并开始计时,当计时器达到传入的第四个参数大小(这里是1s)时,产生一个中断信号,中断当前正在进行的其他进程,进行上下文切换恢复hello的上下文信息,并把hello进程从等待队列中移出,从内核模式转为用户模式,此时hello进程就可以继续执行逻辑控制流了。
此外当循环结束后,程序调用getchar系统级函数时会进入陷阱处理(实际上是因为执行了read函数的系统调用),由用户模式进入内核模式,内核中的陷阱处理程序请求来自键盘缓冲区的信号传输,并执行上下文切换把控制转移给其他进程,数据传输结束之后,引发一个中断信号,内核从其他进程上下文切换到hello进程,控制回到hello进程中。
6.6 hello的异常与信号处理
hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
1. 中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
2. 陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
3. 故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
4. 终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将终止这个应用程序。
hello对于信号处理的分析:
- 正常终止,会向父进程发送SIGCHLD信号。通过ps指令可以看出运行完后程序被回收。
图6-2
- 随便乱按,在进行乱按的时候,第一个字符串会被读入,而其他的字符串会在缓冲区中储存,然后再程序结束的时候,当成命令行命令读入.
图6-3
- 运行过程中按下Ctrl-C会向进程发送SIGINT信号。信号处理程序终止并回收进程。
图6-4
- 运行过程中按下Ctrl-Z ,shell进程会收到SIGSTP信号,信号处理函数将hello进程挂起,通过ps命令我们可以看出hello进程没有被回收,其进程号为5963,用jobs命令看到job ID是1,状态是Stopped,使用fg 1命令将其调到前台,此时shell程序首先打印hello的命令行命令,然后继续运行打印剩下的信息,此时再暂停进程用pstree查看该进程位置,接着我们可以顺便用kill -9 5963发送SIGINT信号终止该进程,此时用ps查看该进程被终止,之后被shell回收。
图6-5
图6-6
图6-7
6.7本章小结
本章解释了hello进程的执行过程。主要介绍了阐述了shell的作用,hello的创建、加载(fork,execve)和信号以及异常处理。
总的来说,程序是指令、数据及其组织形式的描述,进程是程序的实体,是运行的程序。在hello运行过程中,内核对其进行管理,决定何时进行上下文切换,当接受到不同的信号时,异常处理程序将对异常信号做出响应,执行相应的代码。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:程序代码经过编译后出现在汇编程序中地址。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
虚拟地址:CPU启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的N个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。
物理地址:CPU通过地址总线的寻址,找到的真实的物理内存对应地址。
结合hello程序来说,逻辑地址指的是是hello.o中的汇编语句中出现的地址。线性地址指的是偏移量再加上代码段的段地址(线性地址)如下图中main左边的内容,之后在运行时取数据的时候经过MMU的处理后将得到实际存储在计算机存储设备上的数据地址,这时得到的是物理地址。
图7-1
7.2 Intel逻辑地址到线性地址的变换-段式管理
在 Intel 平台下,逻辑地址(logical address)采用的时 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT( 全局描述符表 ) 里拿到 segment base address(段基址) 然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。
一个逻辑地址由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。我们可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中[5]。
如下图,当给定一个完整的逻辑地址段选择符+段内偏移地址时,先看段选择符的T1=0还是1,及知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。首先拿出段选择符中前13位,然后在这个数组中查找到对应的段描述符,得到其基地址(Base)。最终我们可以通过Base + offset = 线性地址。
图7-2[5]
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理的数据结构:
由csapp书中所说:虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。磁盘上数组的内容被缓存在物理内存中。这些内存块被称为页。虚拟页存在未分配的、缓存的、未缓存的三种状态。其中缓存的页对应于物理页。
图7-3
页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表中一个固定偏移量处都有一个PTE。假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
图7-4
页式管理地址变换:
MMU利用VPN来选择适当的PTE,将列表条目中PPN和虚拟地址中的VPO串联起来,就得到相应的物理地址。首先我们先将线性地址分为 VPN+VPO的形式。然后再将 VPN 拆分成 TLBT+TLBI然后去 TLB 缓存里找所对应的 PPN(物理页号)将其与VPO最后进行组合即可。
图7-5
7.4 TLB与四级页表支持下的VA到PA的变换
为了消除每次CPU产生一个虚拟地址MMU就查阅一个PTE带来的时间开销,许多系统都在MMU中包括了一个关于PTE的小的缓存,称为TLB,TLB的速度快于L1 cache。
为了减少页表太大而造成的空间损失,可以使用层次结构的页表页压缩页表大小。
图7-6
Core i7使用的是四级页表。如下图所示,在四级页表层次结构的地址翻译中,虚拟地址被划分为4个VPN和1个VPO。每个第i个VPN都是一个到第i级页表的索引,第j级页表中的每个PTE都指向第j+1级某个页表的基址,第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。因此为了构造物理地址,在能够确定PPN之前,MMU必须访问四个PTE。
图7-7
7.5 三级Cache支持下的物理内存访问
在现代计算机中,存储器被组织成层次结构,这样可以最大程度地平衡访存时间和存储器成本。CPU在访存时并不是直接访问内存,而是访问内存之前的三级cache。以Core i7为例其的三级cache是物理寻址的,块大小为64字节。LI和L2是8路组相联的,而L3是16路组相联的。
通过前两节的过程,得到了对应的52位物理地址。接下来CPU把地址发送给L1。我们以L1的寻址过程为例其余与其类似。
因为L1块大小为64字节,所以B=64,b=6。又由于L1是8路组相联的,所以S=8,s=3。计算得标记位t有52-6-3=43位,及 52位物理地址的前43位。
接下来首先,根据物理地址的s位组索引索引到L1 cache中的某个组,然后在该组中查找是否有某一行的标记等于物理地址的43位标记且该行的有效位为1,若有,则说明命中,从这一行对应物理地址对应块偏移的位置取出一个字节。若不满足上面的条件,则说明不命中,需要继续访问下一级cache。
接下来的Cache访问的原理与L1相同,若是三级cache都没有要访问的数据,则需要访问内存,从内存中取出数据并放入cache。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rMyfhM1G-1652980513712)(media/336dd3749df8df023e894c396c34b1c1.png)]图7-8
7.6 hello进程fork时的内存映射
当fork 函数被 shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
图7-9
7.7 hello进程execve时的内存映射
execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。
其大概步骤为:
1. 删除已存在的用户区域。
2. 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
3. 映射共享区域,hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
4. 设置程序计数器(PC),使之指向代码区域的入口点。
图7-10
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。
图7-11
先检测该内存访问是否合法,若访问是不合法的,那么缺页处理程序会触发一个保护异常,终止这个进程。
若此时内核知道该操作是合法的,那么将把对应页面加载并更新页表。先选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令再次读取对应的地址中的数据。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块保持空闲,直到它显式地被应用所分配,一个已分配的块保持已分配状态,直到它被释放。
分配器的类型有两种:显式分配器和隐式分配器。显式分配器:要求应用显式地释放任何已分配的块。例如,C语言中的malloc函数申请了一块空间之后需要free函数释放这个块。隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块。比如Java,ML和Lisp等高级语言中的垃圾收集。
隐式空闲链表分配器管理:
带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、(可能的)额外填充以及一个字的尾部组成。空闲块通过头部的大小字段隐含地连接着。分配器遍历堆中所有的块,间接地遍历整个空闲块的集合。当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配和最佳适配。分配器在面对释放一个已分配块时,可以合并相邻的空闲块,其中一种简单的方式,是利用隐式空闲链表的边界标记来进行合并。
图7-12
显式空间链表的基本原理:
显式空闲链表是将堆的空闲块组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。进行内存管理。在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
图7-13
7.10本章小结
本章主要介绍了hello的存储地址空间、intel的段式管理、hello的页式管理,以及TLB与四级页表支持下的VA到PA的变换过程和三级Cache支持下的物理内存访问,hello进程fork和execve时的内存映射、缺页故障的处理流程和动态存储分配器的管理。
(第7章 2分)
第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。这个文件位置是从文件开头起始的字节偏移量。
4. 读写文件:一个读操作就是从文件复制n>0个字节到内存,写操作就是从内存中复制n>0个字节到一个文件。
5. 关闭文件:内核释放打开文件时创建的数据结构以及占用的内存资源,并将描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
UnixI/O函数:
- int open(char* filename, int flags, mode_t mode);open函数将filename转换为一个
文件描述符,并且返回描述符数字, 返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件。
2. intclose(int fd);关闭一个打开的文件,返回操作结果。
3. ssize_t read(int fd, void *buf, size_t n);read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
4. ssize_t write(int fd, const void *buf,size_t);write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
8.3 printf的实现分析[8]
printf函数的实现如下,首先arg获得第二个不定长参数,即输出的时候格式化串对应的值。
- 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;
- }
接着看vsprintf函数,可以看出vsprintf程序按照格式fmt,结合参数args生成格式化之后的字符串,并返回字串的长度。在printf中调用系统函数write(buf,i)将长度为i的buf输出。
- 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);
- }
接着在printf中调用系统函数write(buf,i)将长度为i的buf输出。
接下来分析write的汇编代码:
- write:
- mov eax, _NR_write
- mov ebx, [esp + 4]
- mov ecx, [esp + 8]
- int INT_VECTOR_SYS_CALL
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
查看syscall的实现如下。syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中。显存中存储的是字符的ASCII码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。于是我们的打印字符串就显示在了屏幕上。
- sys_call:
- call save
- push dword [p_proc_ready]
- sti
- push ecx
- push ebx
- call [sys_call_table + eax * 4]
- add esp, 4 * 3
- mov [esi + EAXREG - P_STACKBASE], eax
- cli
- ret
8.4 getchar的实现分析[7]
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar调用了read函数,read函数也通过sys_call调用内核中的系统函数,将读取存储在键盘缓冲区中的ASCII码,直到读到回车符,然后返回整个字符串,getchar函数只从中读取第一个字符,其他的字符被缓存在输入缓冲区。
8.5本章小结
本章介绍了Linux中I/O设备的管理方法,UnixI/O接口和函数,分析了printf和getchar函数如何通过UnixI/O函数实现。
(第8章1分)
结论
至此为止我们已经正式走完了hello程序的一生。正如一个人的一生一样但是hello的一生所做到的事情并不是光一个程序就能成就的,其包含了几代程序员与底层设计人员的心血让其跑的更快更稳定,同时也让它结束之后不会对别的程序造成坏的影响。
总结hello的一生的话大概可以分为以下几个阶段:
首先hello的程序代码被程序员写完并保存为 hello.c,之后 hello.c 经过预处理器所有的预处理命令解析得到了 hello.i,之后 hello.i 经过编译器翻译变为了成汇编程序 hello.s,然后汇编程序 hello.s 经过汇编器翻译变为了二进制文件 hello.o,最后链接器将 hello.o 与各种库链接为可执行目标程序 hello,从此hello正式进入了可以被电脑执行的机器指令阶段,从这里开始正式进入其与电脑合作的一生。
此后用户用 shell 输入参数运行 hello,然后shell 用 fork 和 execve 创建 hello 的子进程。为 hello的执行创建条件:创建地址区域,程序入口进行初始化,然后转入 main 函数从此CPU 将一步步执行 hello 的指令。
hello在运行时会涉及到访存操作此时MMU 把虚拟地址翻译成物理地址,然后通过三级 cache等内存结构读出数据。同样其一生不可能一帆风顺,运行时可能 会有许多异常,比如用户输入的ctrl+z,hello 中的sleep函数等,都会触发上下文切换,同样的还有getchar 函数,其会调用 read 触发中断异常将控制转入操作系统。最后hello 进程结束,shell 回收子进程,hello 进程就淡出了我们的视野。
最后正如学习CSAPP的一样,hello的一生涉及到的知识很多,让我从小到大的简单认识到了计算机系统以及计算机底层的知识。
附件
hello.c——原文件
hello.i——预处理之后文本文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标执行
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf格式,用来看hello.o的各节信息
hello.dump——hello.o的反汇编文件,用来看汇编器翻译后的汇编代码
hello.objdump——hello的反汇编文件,用来看链接器链接后的汇编代码
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
- C语言预处理命令分类和工作原理https://baijiahao.baidu.com/s?id=1687923180513685219&wfr=spider&for=pc
- 编译原理概念https://blog.csdn.net/qq_46106285/article/details/122162937
- 编译链接目标文件里面有什么,ELF文件结构描述https://blog.csdn.net/xiaosaerjt/article/details/100546667
- Shell(bash) 介绍https://blog.csdn.net/liaowenxiong/article/details/115763500
- 段页式访存——逻辑地址到线性地址的转换http://t.zoukankan.com/pipci-p-12403976.html
- 兰德尔 E.布莱恩特. 深入理解计算机系统. 龚奕利 译.
- 库函数getchar()详解https://blog.csdn.net/hulifangjiayou/article/details/40480467
- printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)
MMU 把虚拟地址翻译成物理地址,然后通过三级 cache等内存结构读出数据。同样其一生不可能一帆风顺,运行时可能 会有许多异常,比如用户输入的ctrl+z,hello 中的sleep函数等,都会触发上下文切换,同样的还有getchar 函数,其会调用 read 触发中断异常将控制转入操作系统。最后hello 进程结束,shell 回收子进程,hello 进程就淡出了我们的视野。
最后正如学习CSAPP的一样,hello的一生涉及到的知识很多,让我从小到大的简单认识到了计算机系统以及计算机底层的知识。
附件
hello.c——原文件
hello.i——预处理之后文本文件
hello.s——编译之后的汇编文件
hello.o——汇编之后的可重定位目标执行
hello——链接之后的可执行目标文件
hello.elf——hello.o的elf格式,用来看hello.o的各节信息
hello.dump——hello.o的反汇编文件,用来看汇编器翻译后的汇编代码
hello.objdump——hello的反汇编文件,用来看链接器链接后的汇编代码
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
- C语言预处理命令分类和工作原理https://baijiahao.baidu.com/s?id=1687923180513685219&wfr=spider&for=pc
- 编译原理概念https://blog.csdn.net/qq_46106285/article/details/122162937
- 编译链接目标文件里面有什么,ELF文件结构描述https://blog.csdn.net/xiaosaerjt/article/details/100546667
- Shell(bash) 介绍https://blog.csdn.net/liaowenxiong/article/details/115763500
- 段页式访存——逻辑地址到线性地址的转换http://t.zoukankan.com/pipci-p-12403976.html
- 兰德尔 E.布莱恩特. 深入理解计算机系统. 龚奕利 译.
- 库函数getchar()详解https://blog.csdn.net/hulifangjiayou/article/details/40480467
- printf 函数实现的深入剖析 https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)