摘 要
本文从hello.c源代码开始,分两部分介绍hello程序的在计算机系统中的整个生命周期。
首先,利用gcc和gdb分析hello.c在预处理、编译、汇编、链接这每个阶段的变化。当编译完成,生成可执行文件hello后,本文又从操作系统进程的概念出发,分析hello的进程管理、内存管理以及Unix I/O的交互。
关键词: 编译原理;操作系统;进程;虚拟内存;深入理解计算机系统
第1章 概述
1.1 Hello简介
hello程序的生命周期是从一个高级C语言程序开始的。为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。这样的转化成为编译,最后得以作为进程在计算机系统中运行,也就是P2P过程。
hello的P2P过程是GCC调用cpp(预处理器)/cc1(编译器)/as(汇编器)/ld(连接器),将C语言源文件预处理、编译、汇编、链接,最终生成可执行文件保存在磁盘中。
hello的020是Hello可执行目标程序从运行到最后被回收的过程。在Shell中运行该程序时,Shell调用fork函数创建子进程,创建完毕后,操作系统内核提供的execve函数会创建虚拟内存的映射,即mmp,然后开始加载物理内存,进入到main函数当中执行相关的代码,打印出信息。在进程中,TLB、4级页表、3级Cache,Pagefile等等设计会加快程序的运行。程序运行完成后,Shell回收子进程,操作系统内核删除相关数据结构,释放其占据的资源。至此,hello的一生就结束了。
1.2 环境与工具
1.2.1硬件环境
CPU:Intel® Core™ i7-10875H CPU @ 2.30GHz 2.30 GHz
RAM:16.0 GB
1.2.2操作系统
Windows 11 21H2、Windows Subsystem for Linux2(Ubuntu 20.04 LTS)
1.2.3开发工具
Vscode、vim、gcc、gdb、edb
1.3 中间结果
为编写本论文,生成的中间结果文件以及它们的作用如下:
文件 | 作用 |
---|---|
hello.c | 源代码 |
hello.i | 预处理后的代码 |
hello.s | 汇编代码 |
hello.o | 可重定位目标文件 |
hello.o.elf.txt | hello.o的ELF |
hello.o.s | hello.o反汇编后的代码 |
hello | 链接后的可执行目标文件 |
hello.elf.txt | hello的ELF |
obj_hello.s | hello的反汇编代码 |
表格 1 中间结果文件及作用
1.4 本章小结
本章大致介绍了hello的P2P和020过程,描述了使用的环境与工具,并列出了生成的中间结果文件以及它们的作用。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 什么是预处理
在编译和链接hello.c之前,需要对源文件进行一些文本方面的操作,比如文本替换、文件包含、删除部分代码等,这个过程叫做预处理,由预处理程序完成。
2.1.1 预处理的作用
预处理根据以字符#开头的命令,修改原始的C程序。比如我们初学C语言时记忆最深刻的代码:
#include<stdio.h>
它就利用了预处理:引用头文件。它告诉预处理器读取系统头文件stdio.h的内容,预处理器把它插入程序文本中。ANSI标准定义的c语言预处理指令有以下这些:
图 2 预处理指令
2.2在Ubuntu下预处理的命令
Linux下使用gcc预处理的命令为:
gcc –E hello.c –o hello.i
-E 表示只激活预处理过程
2.3 Hello的预处理结果解析
2.3.1生成文件对比
使用vscode打开文件,hello.c源程序只有24行,如下:
打开生成的hello.i文件,发现有3060行,并且我们原始的main代码部分被放在最后
图 4 hello.i文件
2.3.2预处理后的文件解析
- 外部库文件
首先,开始部分有一系列外部库.h文件路径
图 5 hello.i外部库
- 数据类型名称替换
接下来是一堆typedef,前面是我们编写代码时使用的标准数据类型,而后面的那些别名就是上述讲到的引入的头文件中使用的类型定义
图 6 hello.i类型别名
比如,我们随便打开一个头文件,它也有类似的行为:
图 7 types.h示例
- 内部函数声明
中间部分是很多内部函数的声明,包括系统内核提供的接口的封装:
图 8 hello.i内部函数声明
- 主体代码
而一直到最后,才是我们写的main函数代码部分
图 9 hello.i主体代码部分
2.4 本章小结
本章介绍了hello.c的预处理过程,并分析了预处理的结果文件hello.i。
从程序员的角度来说,利用宏定义指令可以让我们轻松写出可读性更好的代码,利用条件编译指令可以让我们更加方便快捷的调试代码。
从hello程序的角度来说,hello.c是残缺的,不完整的,预处理阶段使它健全了四肢,得以最终运行在操作系统的上下文中。
第3章 编译
3.1 编译的概念与作用
3.1.1什么是编译
汇编语言是对硬件的抽象,而C语言又是对汇编语言的抽象,C语言对人友好,但对机器并不友好。编译阶段正是把完整的代码hello.i翻译成对应的汇编语言程序hello.s
3.2 在Ubuntu下编译的命令
Linux下使用gcc编译的命令为:
gcc -S hello.i -o hello.s
-S表示只激活到编译过程
图 10 编译命令
3.3 Hello的编译结果解析
hello.i编译生成了对应的汇编代码,在这一节中,我将对C语言中数据类型及各式操作如何编译到汇编代码中逐个解析
3.3.1常量
- 字符型常量
图 11 字符型常量位置
printf中打印了一个字符串,这个字符串常量存在.LC0中:
图 12 hello.s中的字符型常量
- 其它常量
还有一些其它常量直接在汇编代码中以立即数的身份出现,例如这段代码,有一个整型常量4
图 13 整型常量位置
它对应的汇编代码如下:
图 14 整型常量在汇编中的形式
cmpl比较argc与4是否相等,如果不相等,才把上述的字符串常量加载到寄存器中
3.3.2变量与运算
- 局部变量
局部变量存储在寄存器或者栈中。
hello.c中有一个局部变量:
图 15 局部变量位置
i是在一个for循环语句中作为循环变量,这段代码如下:
图 16 局部变量在hello.s中的形式
可以看到,i存储在栈中。
- 算数操作
同时在上述的for循环中,局部变量i的值每次加1,这个运算就由addl指令来完成:
图 17 addl指令
3.3.3数组/指针操作
main函数的参数中,有一个字符串数组:
图 18 main函数参数
其中,argc是输入的参数的个数,也就是字符串数组argv中的元素个数。
根据下面这行代码:
图 19 sleep参数
找到其对应的汇编代码为:
图 20 sleep汇编代码
%rdi是第一个参数寄存器,也就是存储函数atoi调用的参数,所以栈中%rbp-8指向的栈空间存的内容就是argv[3]的地址
同理,观察以下代码:
图 21 汇编代码
经过分析,分别找到argv[1]的地址存放在%rbp-16指向的栈空间中,argv[2]的地址存放在%rbp-24存放的栈空间中。
3.3.4控制转移
同样还是以上述那段for循环的代码为例,循环变量i从0开始,每次循环都要加1,并在循环开始判断i<8,对应的汇编代码就是用cmpl指令,判断i是否小于等于7,如果是,则继续执行循环体中的内容,如果不是则跳出循环。
3.3.5函数调用与返回
main函数
传入参数为argc,和argv,为系统调用,且参数从Shell中传入,返回值设置为0
- printf函数
通过设置寄存器%rdi和%rsi的值来传入参数并调用
图 22 printf函数调用
- exit函数
通过设置寄存器%rdi和%rsi的值来传入参数并调用
图 23 exit函数调用
- atoi函数
将一个字符串的首地址赋给%rdi调用,函数返回这个字符串转成的整数值,存放在字符串%eax
图 24 atoi函数调用
3.4 本章小结
本章介绍了从hello.i文件编译成hello.s文件的过程,以及原始的.c文件中各部分变量、常量、控制转移以及函数调用在汇编语言中是什么样子。
接下来,只需要将hello.s稍加改造(汇