计算机系统基础
——程序人生-Hello’s P2P
Hello world的一生由hello.c文件开始,经过被gcc整合的功能块cpp(预处理器),ccl(编译器),as(汇编器)之后变为可重定位的目标文件,再经由ld(链接器)的符号解析和重定位之后成功变为可执行目标文件。本文通过分析一个hello.c的完整的生命周期,从它开始被编译,到被汇编、链接、在进程中运行,讲解了Linux计算机系统执行一个程序的完整过程。
关键词:预处理, 编译, 汇编, 链接, 进程, 虚拟内存
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.2 环境与工具
硬件环境
X64 CPU;2GHz;4G RAM;256GHD Disk
软件环境
Windows10 64位;VMware 14;Ubuntu 18.04
开发工具
Visual Studio 2017 64位;CodeBlocks;vim,gpedit+gcc,as,ld,edb,readelf,HexEdit
1.3 中间结果
hello.i —— 修改了的源程序(文本)
hello.s —— 汇编程序(文本)
hello.o —— 可重定位目标程序(二进制)
hello —— 可执行目标程序(二进制)
1.4 本章小结
本章简要的概括了hello world一生的两个阶段:P2P与020的过程,以及进行实验时的软硬件环境及开发与调试工具,以及在本论文中生成的中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念
在编译之前进行的处理。预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
2.1.2预处理的作用
预处理主要体现在宏定义、文件包含、条件编译三个方面,预处理命令以符号“#”开头。预处理会导入宏定义与文件、头文件中的内容,使得程序能完整、正常的运行,预处理生成了hello.i的源代码文本文件。
2.2在Ubuntu下预处理的命令
通过输入gcc hello.c -E -o hello.i可以对hello.c进行预处理,得到hello.i
2.3 Hello的预处理结果解析
在预处理文本文件hello.i中,首先是对文件包含中系统头文件的寻址和解析
hello.i文件可以看到,它的前面头文件等等被展开了,变成了很多以#开头的内容,在原有代码的基础上,将头文件stdio.h的内容引入,例如声明函数、定义结构体、定义变量、定义宏等内容。hello.i的最下面是我们熟悉的C语言程序。
2.4 本章小结
本章通过了解预处理的概念及作用,进行Ubuntu下预处理操作,并讲述了编译器的工作,以及我们怎样在Ubuntu下将一个与处理文件变为一个汇编代码文件,解释了在汇编代码中是如何实现c语言中的各项数据和指令的。
第3章 编译
3.1 编译的概念与作用
3.1.1编译的概念
编译过程就是将预处理后得到的预处理文件进行词法分析、语法分析、语义分析、优化后,生成汇编代码文件,本文中是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序的过程,生成一个hello.s的汇编语言源程序文件。
3.1.2编译的概念
编译的作用是将高级语言转变为更易于计算机读懂的汇编语言,同时它还可以进行语法检查、程序优化。
3.2 在Ubuntu下编译的命令
Ubuntu终端下进入hello.c所在文件,输入指令gcc hello.i -S -o hello.s,按下回车即可。
3.3 Hello的编译结果解析
得到的hello.s就是编译后得到的汇编语言源程序文件,通过gedit查看它,发现它的头部声明了全局变量和它们存放的节段,接下来是将源程序的命令汇编得到的代码,接下来对它们进行分析。
3.3.1 全局变量
在hello.c中有一个全局变量sleepsecs,它被定义成int型,但在编译器编译的过程中将它优化为了long型,这里编译器进行了隐式的类型转换,我们给它赋值为2.5,查看sleepsecs的值时发现sleepsecs = 2,它被存放在.rotate节中。
3.3.2 局部变量
通过观察这里,我们发现在.L2中声明了一个局部变量i,将其存储在-4(%rbp)中,可以得知在处理局部变量时,编译到当前位置才去申请这样一个内存空间的。
3.3.3 赋值
程序中涉及的赋值操作有:
int sleepsecs=2.5 :因为sleepsecs是全局变量,所以直接在.data节中将sleepsecs声明为值2的long类型数据。
i=0:整型数据的赋值使用mov指令完成,根据数据的大小不同使用不同后缀,分别为:
指令 b w l q
大小 8b (1B) 16b (2B) 32b (4B) 64b (8B)
因为i是4B的int类型,所以使用movl进行赋值。
3.3.4 类型转换
程序中涉及隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的2.5转换为int类型。当在double或float向int进行类型转换的时候,程序改变数值和位模式的原则是:值会向零舍入。例如1.999将被转换成1,-1.999将被转换成-1。进一步来讲,可能会产生值溢出的情况,与Intel兼容的微处理器指定位模式[10…000]为整数不确定值,一个浮点数到整数的转换,如果不能为该浮点数找到一个合适的整数近似值,就会产生一个整数不确定值。
浮点数默认类型为double,所以上述强制转化是double强制转化为int类型。遵从向零舍入的原则,将2.5舍入为2。
3.3.5 算术操作
汇编语言中加减乘除四则运算是通过语句来实现的:
指令效果
leaq S,D D=&S
INC D D+=1
DEC D D-=1
NEG D D=-D
ADD S,D D=D+S
SUB S,D D=D-S
IMULQ S R[%rdx]:R[%rax]=SR[%rax](有符号)
MULQ S R[%rdx]:R[%rax]=SR[%rax](无符号)
IDIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(有符号)
R[%rax]=R[%rdx]:R[%rax] div S
DIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(无符号)
R[%rax]=R[%rdx]:R[%rax] div S
i++,对计数器i自增,使用程序指令addl,后缀l代表操作数是一个4B大小的数据。
汇编中使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi。
3.3.6 关系操作
关系操作就是比较两个变量的大小情况,通过cmpl来执行,在cmpl中比较两个数的大小,用后一个数减去前一个数得到结果的情况来设置标志位,接下来可以通过设置的标志位进行跳转等操作。
3.3.7 控制转移之条件语句
通过cmpl进行比较,根据比较的结果通过jx进行跳转,跳转方式可以通过查看跳转表得到
3.3.8 控制转移之循环语句
这里就是一个循环语句的开始,可以发现我们的循环条件是i < 10,在这里被优化为了i <= 9,每次将计数器的值与9进行比较,若小于等于则跳转到循环内部L4执行.
循环内部语句如下,在每次执行.L4结束后都将-4(%rbp)加1,因此它是i,起到一个计数器的作用。
3.3.9 函数操作
参数传递:在函数的参数传递中使用不同的寄存器来保存第x个参数
函数调用:使用call语句来实现函数的调用。
函数返回:函数的返回值保存在%rax中,将需要返回的变量值存在%rax中,在进行函数的操作之后ret即可返回%rax中的值。
程序中涉及函数操作的有:
main函数:
传递控制,main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址dest压栈,然后跳转到main函数。
传递数据,外部调用过程向main函数传递参数argc和argv,分别使用%rdi和%rsi存储,函数正常出口为return 0,将%eax设置0返回。
分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP,将下一条要执行指令的地址设置为dest。
printf函数:
传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。
控制传递:第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf使用call printf@PLT。
exit函数:
传递数据:将%edi设置为1。
控制传递:call exit@PLT。
sleep函数:
传递数据:将%edi设置为sleepsecs。
控制传递:call sleep@PLT。
getchar函数:
控制传递:call gethcar@PLT
3.4 本章小结
本阶段完成了对hello.i的编译工作。使用编译指令可以将其转换为.s汇编语言文件。完成该阶段转换后,可以进行下一阶段的汇编处理。