HITICS 深入理解计算机系统大作业
摘要
本文简单介绍了hello从.c文件经历了.i,.s,.o等中间文件形式,被转换成可执行目标文件的过程。又简述了shell如何通过创建子进程运行这个可执行目标文件,一直到这个程序运行结束的过程,可谓是“hello的程序人生”
关键词:程序;预处理;编译;汇编;链接;异常;虚拟内存;Unix IO
第1章 概述
Hello简介
P2P:hello.c经过cpp,ccl,as,ld的处理由一个源代码文件转换成了一个可执行目标文件,shell fork子进程运行这个可执行目标文件
020:子进程通过execve执行这个可执行目标文件,中间经过异常、信号、虚拟内存管理等等步骤,最终运行结束,被父进程回收,抹去存在的痕迹。
1.1环境与工具
XPS 15 9570
VMWARE
UBUNTU
WINDOWS
EDB
NOTEPAD++
VIM
READELF
1.2 中间结果
文件 | 功能 |
---|---|
hello.i | 预处理后的ASCII码文件 |
hello.o | 汇编文件 |
hello.elf | 可重定位目标文件 |
helloasm.txt | hello.o 反汇编代码 |
hello | 最终的可执行目标文件 |
helloasmm.txt | hello 反汇编代码 |
helloelf_lk.txt | hello的部分elf内容 |
elfall.txt | hello的全部elf内容 |
1.3 本章小结
这篇论文要开始了,介绍的hello的程序人生。
第2章 预处理
预处理的概念与作用
C语言标准规定,预处理是指前4个编译阶段(phases of translation)。
1.三字符组与双字符组的替换
2.行拼接(Line splicing): 把物理源码行(Physical source line)中的换行符转义字符处理为普通的换行符,从而把源程序处理为逻辑行的顺序集合。
3.单词化(Tokenization): 处理每行的空白、注释等,使每行成为token的顺序集。
4.宏扩展与预处理指令(directive)处理.
2.2 在Ubuntu下预处理的命令
cpp hello.c > hello.i
2.3 Hello的预处理结果解析
在这里可以看到.i文件变成了一个3188行的大文件,前面就是各种库的展开。
比如这里
是stdlib.h,里面有很熟悉的老朋友:
其中还有很多#ifdef,cpp会根据#ifdef后面的值到底有没有定义过来决定是否编译其中的语句。
2.4 本章小结
本章主要介绍了预处理的概念与作用,给出了预处理命令,对hello.c预处理之后的hello.i文件进行解析。
课程当中并没有着重讲过预处理这个环节,仅在链接开头提过,因此限于本人水平问题,不给出详细说明。
第3章 编译
3.1 编译的概念与作用
在整个编译过程中,编译器会完成大部分的工作,将把C语言提供的相对比较抽象的执行模型表示的程序转化成处理器执行的非常基本的指令。
3.2 在Ubuntu下编译的命令
gcc -s hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.0 伪指令
所有以’.’开头的行都是指导汇编器和连接器工作的伪指令。(我们通常可以忽略这些行。)但在这里不能忽略……
其中:
.file声明了源文件的名字
.text声明代码段
.data rodata 声明只读数据段
.globl声明一个全局变量
.type声明变量类型(function or object)
.size声明变量大小
.long 声明一个long类型
.string 声明一个string类型
.align声明对齐大小要求
3.3.1 数据
Hello.s用到的数据类型有整数(int argc, int i, int sleepsecs,immediate), 字符串(“Usage: Hello 学号 姓名!\n”,“Hello %s %s\n”),数组(char *argv[])
3.3.1.1 整数
Hello.c里用到的整数有:int argc, int i, int sleepsecs,其中:
- argc是main传进来的参数,代表的是命令行中的参数个数。
- i是定义的局部变量,存储在栈中
如图所示,因为在一开始定义的时候并未初始化,所以没有立即将i的值压入栈中,在for循环中定义了i从初值为0开始循环,此时将0压入栈中 - sleepsecs是定义的全局变量(int sleepsecs = 2.5)
可以看出sleepsecs是一个全局变量,对齐要求是4字节,是一个对象类型的变量,大小是4个字节,定义为一个long类型的变量,值为2,因为linux系统里面int跟long都是4个字节,所以可以互换地使用。
4.Immediates,例如判断是否进入if分支中的立即数3:
3.3.1.2 字符串
Hello.c用到了两个字符串(前面已列出)
都是定义在.rodata节中,表明是只读不可修改,都是用于printf输出的字符串
3.3.1.3 数组
用到的数组是char *argv[],是命令行传进来的参数,argv作为第二个参数传进来,所以存在%rsi中。
Argv是命令行传入的参数,数组中的每个元素都指向一个字符串,一般来说argv[0]指向的是执行的文件的名字。
3.3.2 赋值操作&逗号操作
程序中涉及到了’=’赋值操作和逗号操作符
3.3.2.1 赋值操作
程序中的赋值操作用“=”来完成
全局变量sleepsecs的赋值直接在.data段的声明中完成
局部变量i的赋值用汇编指令mov来完成
3.3.2.2 逗号操作符
逗号操作符是指在C语言中,多个表达式可以用逗号分开,其中用逗号分开的表达式的值分别结算,但整个表达式的值是最后一个表达式的值。在这里面是printf中的分别打印各自指定的值
3.3.3 类型转换
涉及到的类型转换是int sleepsecs = 2.5,因为是个int(long)类型,所以直接就将小数点之后抹去了,声明的时候也是声明为2.
3.3.4 算术操作
程序中C代码涉及到的算术操作有:++
汇编代码中涉及到的算术操作有:subq,addq,leaq
3.3.4.1 C代码中的算术操作
1.i++,即自增操作,约等价于i= i+1,先引用后自增、
3.3.4.2 汇编代码中的算术操作
1.subq A,B
表示B-A,结果存放在B中,A,B可以是内存位置、立即数、寄存器,但不能同时是内存位置
2.addq A, B
表示A+B,结果存放在B中,A,B可以是内存位置、立即数、寄存器,但不能同时是内存位置
3.leaq A,B
本身表示加载有效地址,把A的地址加载到B中,但因为是做加减乘混合运算,有时也可以用于算术运算
3.3.5 关系操作
程序中C代码涉及的关系操作有:!=,<
汇编代码中涉及的关系操作有:cmp
3.3.5.1程序C代码中涉及的关系操作
程序中C代码涉及的关系操作有:!=,<
汇编代码中涉及的关系操作有:cmp
1.!=
用于判断是否进入分支
2.<
用于判断是否进入/跳出循环
3.3.5.2程序汇编代码中涉及的关系操作
1.cmp A, B
执行隐含的减法操作,B-A但不保存结果,根据结果设置标志位,修改OF,SF,ZF,CF,AF,PF
3.3.6 控制转移
程序C代码涉及的控制转移有:if语句,for循环
程序汇编代码中涉及的控制转移有:jmp类
3.3.6.1 程序C代码涉及的控制转移
1.if语句
2.for循环
3.3.6.1 程序汇编代码涉及的控制转移
Cmpl将压入栈中的第二个参数与3作比较,并设置条件码,如果ZF = 1就跳转,否则执行下一条指令
2.jmp
无条件跳转,跳到.L3标记的位置
3.jle
Cmpl设置条件码,将9和局部变量i进行比较,如果ZF = 1或OF = 1就进行跳转,否则继续执行下条指令
3.3.7 函数操作
函数操作本质上是一个用户层面的过程调用,涉及到参数传递,函数调用,函数返回
3.3.7.1 参数传递
X86-64在调用函数之前,会将前六个参数传递给寄存器,剩下的参数(若有)压入栈中
如图所示,调用sleep函数之前将需要睡眠的秒数通过寄存器%rdi传递给了函数sleep,因为sleep是动态链接库中的函数,所以需要使用PLT定位,此为链接内容,后话……后话……
3.3.7.2 函数调用
在函数调用之前,会将返回地址(即当前pc值)压入栈中,使得函数调用结束之后能够返回。
3.3.7.3函数返回
在调用函数执行结束之后,若无返回值则直接返回,若有返回值则将返回值放入%rax寄存器之后返回
3.3.7.3 程序中涉及的函数调用
1.main
参数传递:外部调用会将argv和argc分别通过寄存器%rdi和%rsi传入main函数
函数调用:一个新的进程开始执行某一可执行目标文件时会先调用__libc_start_main,是系统启动时调用的函数,call指令会将返回地址压入栈中,然后调用main函数
函数返回:通常来说main函数会返回0
2.printf
参数传递:main函数中将第一条要打印的字符串的地址传入%rdi
因为只有一个字符串所以使用puts
函数调用:因为是动态链接库的函数,需要使用位置无关代码,因此需要GOT和PLT交互在运行时确定函数的地址,所以这里是call puts@PLT/printf@PLT
函数返回:没有返回值
3.exit
参数传递:无
函数调用:因为是动态链接库里的函数,需要使用位置无关代码因此需要GOT和PLT交互在运行时确定函数的地址,所以这里是call exit@PLT
函数返回:不返回,直接退出(这好像是要系统调用)
4.sleep
参数传递:在调用sleep函数之前先将要睡眠的秒数传入寄存器%edi中
函数调用:call指令会在调用之前把返回地址压入栈中,因为是动态链接库里的函数,需要使用位置无关代码因此需要GOT和PLT交互在运行时确定函数的地址,所以这里是call sleep@PLT
函数返回:如果被信号打断,会返回睡眠剩余的秒数,否则就