目录
1.1.1 P2P:From Program to Process
1.1.2 020:From Zero-0 to Zero-0
摘要
本文围绕hello的“程序人生”,对hello“From Program to process”和“From Zero-0 to Zero-0”两个过程展开分析,hello.c从预处理、编译、汇编、链接、fork一系列操作从程序变成进程,再通过调用execve函数加载进程、对控制流的管理、内存空间的分配、异常的处理、对I/O设备的调用、shell父进程回收等一系列操作从无到有再到无。回顾hello的一生,便是一次深入理解计算机系统的历程。
关键词:程序;进程;存储;I/O;程序人生
第1章 概述
1.1 Hello简介
1.1.1 P2P:From Program to Process
在Unix系统上,GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello,这个过程可分为四个阶段——预处理器(cpp)将源程序hello.c修改成hello.i文本文件,编译器(ccl)将hello.i翻译成汇编程序hello.s,汇编器(as)将hello.s翻译成机器语言指令,并将这些指令打包成可重定位目标程序hello.o,连接器(ld)将hello.o与库函数相链接生成可执行目标程序hello。执行该程序时,操作系统调用fork创建一个子进程,此时hello.c就从program变成了一个process。
1.1.2 020:From Zero-0 to Zero-0
execve函数加载并运行可执行目标文件hello,操作系统为其分配虚拟内存空间,在物理内存与虚拟内存之间建立映射。执行过程中,虚拟内存为进程提供独立的空间,数据从磁盘传输到CPU中,TLB、分级页表等保障了数据的高效访问,I/O管理与信号处理共同实现了hello的输入输出。程序运行结束后,shell父进程负责回收hello进程,对应的虚拟空间以及相关数据结构被释放,hello进程便经历了从无到有再到无的过程。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows10 64位以上;VirtualBox 11以上;Ubuntu 16.04 LTS 64位
1.2.3 开发工具
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
hello.c:源程序文件
hello.i:hello.c预处理后的源程序文件
hello.s:hello.i编译后的汇编程序
hello.o:hello.s汇编后的可重定位目标文件
elf.txt:hello.o的ELF文件
hello_o2s.s:hello.o的反汇编代码
hello:hello.o链接后的可执行目标文件
hello_elf.txt:hello的ELF文件
hello_objdump.s:hello的反汇编代码
1.4 本章小结
本章简述了hello的“一生”,即P2P和020,并简单列出此次大作业所需的环境和工具,以及hello.c所生成的中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 概念
在C语言中,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
2.1.2 作用
1.宏替换:将宏名替换为字符串或数值;
2.文件包含:预处理器读取头文件中的内容,并直接插入到程序文本中;
3.条件编译:根据条件编译指令决定需要编译的代码;
4.删除注释。
2.2在Ubuntu下预处理的命令
![](https://i-blog.csdnimg.cn/blog_migrate/58c4947f10567a40c8fb0ccb34331978.png)
2.3 Hello的预处理结果解析
打开预处理后的hello.i文件,发现由原来的23行扩展到3060行。其中头文件中的内容被直接插入到文本文件中,出现大量声明函数、定义结构体、定义变量、定义宏等内容,.c文件中的注释也被删除,但main函数没有改变。
![](https://i-blog.csdnimg.cn/blog_migrate/b22ad78901f0c99f10d9e2c84528a066.png)
2.4 本章小结
本章介绍了预处理的概念和作用,并在Ubuntu中进行预处理,并分析了预处理前后源文件的差别。
第3章 编译
3.1 编译的概念与作用
3.1.1 概念
编译器(ccl)将文本文件hello.c翻译成文本文件hello.s,它包含一个汇编语言程序。
3.1.2 作用
通过以下五个阶段把人们熟悉的高级语言语言换成计算机能解读、运行的低级语言——词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。
1.词法分析:对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
2.语法分析:以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。
3. 语义检查和中间代码生成:由语义分析器完成,指示判断是否合法,并不判断对错。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
4.代码优化:对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
5. 目标代码生成:把语法分析后或优化后的中间代码变换成目标代码。
3.2 在Ubuntu下编译的命令
![](https://i-blog.csdnimg.cn/blog_migrate/c08942c5757d063585520d7adb53789a.png)
3.3 Hello的编译结果解析
3.3.1 数据及数组
在hello.i的main函数中,常量包括printf函数中打印的两个字符串常量和if条件、for循环里的数字常量。其中,如图4所示,字符串常量被存储在./rotate段,如图5所示,数字常量被存储在.text段中,且作为立即数出现。
![](https://i-blog.csdnimg.cn/blog_migrate/9616744bbadd5c1bbf25396cbacde67d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7864ee152805f7472c651dbf267529cb.png)
代码中并没有全局变量,局部变量分别是函数参数argc和argv,以及for循环中的i;其中argc表示参数argv的个数,分析图6、图7可知,argc、数组argv以及i均存储在栈中,argc地址为-20(%rbp); argv首地址为-32 (%rbp),每个参数加8(对应图7绿色方框),即argv[k]=-(32+8k)(%rbp),所以数组取值的操作离不开首地址偏移量;i地址为-4(%rbp)。
![](https://i-blog.csdnimg.cn/blog_migrate/9e2b8ff1767b8caf186680f7b9997778.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6a9ceea016ec5cacf3ada8a8db017474.png)
3.1.2 赋值
如图7左上及右边红色方框所示,左边的hello.i中,需对i进行赋值,即将0赋给i,在汇编代码中对应第一行“movl $0, -4(%rbp)”。
3.1.3 类型转换
如图7蓝色方框所示,hello.i中类型转换体现在“atoi(argv[3])”上,即将字符串参数转换为整型参数,需调用atoi函数,在汇编代码中对应为“call atoi”。
3.1.4 算术操作
如图7右边红色方框,for循环中每次循环i都要自加1,在汇编代码中则由“addl $7, -4(&rbp)”实现。
3.1.5 关系操作及控制转移
如图6及图7右边红色方框所示,if条件中需要用到“!=”和“<”这样的关系操作,if-else或者是否跳出for循环则是控制转移,而在汇编代码中,关系操作往往与控制转移(跳转指令)一起出现,即“cmp”后会跟上“jXX”,跳不跳转则需看cmp设置的条件码,或者是直接跳转指令“jmp”,例如图6中,首先将立即数4与 -20(%rbp)对应值(即argc)进行对比,若等于,即“je”就代表相等则跳转至.L2。
3.1.6 函数操作
hello.i中一共涉及5个函数调用,即main、printf、exit、sleep、getchar,在汇编语言中,寄存器一般都有特定用途,例如%rax存储返回值,%rdi存储第一个参数,%rsi存储第二个参数等。其次,若有多个参数,则将参数存储在栈中,如图8所示,调用函数前都有mov指令,设置不同的参数,最典型的就是调用atoi和sleep函数前都将寄存器%rax中的值传送到储存第一个参数的寄存器%rdi中,而像getchar这样没有参数的函数,则无需执行以上操作。
![](https://i-blog.csdnimg.cn/blog_migrate/5ddbd52be668f3a6412508a6989287cd.png)
再如图9所示,C语言中main函数返回0,那么汇编代码中则对应为“movl $0, %eax”,“leave”,“ret”,即将0传送到%eax,然后使用leave恢复调用者栈帧,清理被调用者栈帧,最后使用ret指令返回。
![](https://i-blog.csdnimg.cn/blog_migrate/038a3bdeebd52be26252bf857d60b6f2.png)
3.4 本章小结
本章围绕编译操作展开,首先介绍了编译的概念和作用,之后在Linux中将hello.i编译成hello.s,并对编译后的hello.s进行分析。