计算机系统课程 笔记总结 CSAPP第三章 程序的机器级表示(3.2-3.4)
3.2 程序编码
- gcc编译器以汇编代码形式产生输出,汇编代码是机器代码的文本表示,给出了程序中的每一条命令。
- 然后gcc调用汇编器和链接器,根据汇编代码生成的可执行机器代码。
- Linux使用了平坦寻址方式,因此可以将整个存储空间(包括栈、堆等)看做一个大的字节数组
执行 gcc -Og -o p p1.c p2.c
- 编译选项 -Og 告诉编译器使用会产生符合原始C代码整体结构的机器代码优化等级
执行 gcc -O1 -o p p1.c p2.c
- -O1 第一级优化
- C预处理器扩展源代码,插入所有用#include指定的文件,并扩展所有用#define定义的宏;
- 编译器产生两个源代码的汇编代码:p1.s和p2.s ;
- 汇编器将汇编代码转化为二进制目标代码:p1.o和p2.o ,目标代码是机器代码的一种形式,包含所有指令的二进制表示,但是还没有填入地址的全局值;
- 链接器将两个目标代码文件与实现函数库的代码合并,并产生最终的可执行文件p;
对于机器级编程,两种抽象尤为重要:
- 机器级程序的格式和行为:
- 定义为指令集体系结构(Instruction set architecture),它定义了处理器状态、指令的格式、每条指令对状态的影响。
- 大多数ISA把程序的行为描述成好像每条指令是按顺序执行的,实际处理器硬件远比描述的精细复杂,它们并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致。
- 机器级程序使用的存储器地址,是虚拟地址:
- 提供的存储器模型看上去是一个非常大的字节数组,存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
- 虚拟内存:将主存和I/O设备抽象成一个非常大的字节数组
- 程序计数器:给出将要执行的下一条指令在内存中的地址
- 整数寄存器:存储地址或整数数据
- 条件码计数器:保存着最近执行的算术或逻辑指令的状态信息
- 向量寄存器:存放一个或多个整数或浮点数值
编译器会将C语言提供的相对比较抽象的执行模型表示的程序转化为处理器执行的非常基本的指令,即汇编代码。汇编代码的表示非常接近于机器代码。
sum:
pushl %ebp
movl %esp,%ebp
movl 12(%ebp),%eax
addl 8(%ebp),%eax
addl %eax,accum
popl %ebp
ret
例如:
int accum = 0 ;
int sum(int x, int y){
int t = x + y;
accum += t;
return t;
}
执行 gcc -O1 -S code.c
将产生code.s
- 代码去除所有关于局部变量名或数据类型的信息
- 有一个对全局变量accum的引用——因为编译器还不能确定这个变量会放在存储器的哪个位置
执行 gcc -O1 -c code.c
将产生code.o
二进制文件
可以找到:
55 89 e5 8b 45 0c 03 45 08 01 05 00 00 00 00 5d c3
- 机器实际执行的程序只是对一系列指令进行编码的字节序列
执行 objdump -d code.o
(反汇编器)
00000000 <sum>:
0:55 pushl %ebp
1:89 e5 movl %esp,%ebp
3:8b 45 0c movl 12(%ebp),%eax
6:03 45 08 addl 8(%ebp),%eax
9:01 05 00 00 00 00 addl %eax,accum
f:5d popl %ebp
10:c3 ret
生成实际可执行的代码需要对一组目标代码运行链接器,这一组目标代码文件中必须含有一个main函数。链接器将代码的地址移动到一段地址范围中,且会确定全局变量的地址:
- 每行前面的地址变更-->
08048394 <sum>:
8048394:55 pushl %ebp
8048395:89 e5 movl %esp,%ebp
8048397:8b 45 0c movl 12(%ebp),%eax
804839a:03 45 08 addl 8(%ebp),%eax
804839d:01 05 00 00 00 00 addl %eax,0x804a018
80483a3:5d popl %ebp
80483a4:c3 ret
- Q:最终链接后生成的文件会比较大?
- A:是因为它不仅包含了多个文件的代码,还包含了用来启动和终止程序的信息,以及用来与操作系统交互的信息。
3.3 数据格式
- 由于是从16位体系结构扩展成32位的,Intel用术语“字(word)”表示16位数据类型。
- 因此,称32位数为“双字(double words)”,称64位数为“四字(quad words)”。
- X86-64指令集包括完整的针对字节、字和双字的指令。
- 汇编代码也使用后缀“l”来表示4字节整数和8字节双精度浮点数。
- 这不会产生歧义——因为浮点数使用的是完全不同的指令和寄存器
3.4 访问信息
- 一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用目的寄存器。
- IA32包含一组8个存储32位值的寄存器,这些寄存器用来存储整数数据和指针。
- 大多数指令有一个或多个操作数用于之处执行一个操作中引用得源数据值,以及放置结果得目标位置。
- 源数据值可以以常数形式(如$12)给出,或者是从寄存器(如%eax)、存储器(如0x804a018)中读出。
- 结果可以存放在寄存器或者存储器中。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 16位操作可以访问最低的2个字节
- 32位操作可以访问最低的4个字节
- 64位操作可以访问整个寄存器
当这些指令以寄存器作为目标时,对于生成小于8字节结果的指令,寄存器剩下的字节:
- 生成1字节和2字节数字的指令会保持剩下的字节不变
- 生成4字节数字的指令会把高位4个字节置为0
3.4.1 操作数指示符
- 大多数指令有一个或多个操作数,指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。
- 源数据值可以以常数形式给出,或是从寄存器或内存中读出。
- 结果可以放在寄存器或内存中。
- 各种不同的操作数的可能性被分为三种类型:
- 立即数:用来表示常数值,立即数的书写方式是'$'后面跟一个标准C表示法表示的整数
- 寄存器:表示某个寄存器的内容。用符号ra来表示任意寄存器a,用引用R[ra]来表示它的值,这是寄存器集合看成一个数组R,用寄存器标识符作为索引。
- 存储器(内存引用):根据计算出来的地址访问某个内存位置。用符号Mb[Addr]表示对存储在内存中从地址Addr开始的b个字节值的引用,为了简便,通常省去下标b。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
有括号-->值 |
|
|
|
|
|
|
|
第一行是立即数,第二行则是寄存器,剩下的全部是存储器。
其中最后一行存储器语法Imm(Eb,Ei,s),表示的是最常用的形式,分为四个部分:
- Imm 立即偏移数
- Eb 基址寄存器
- Ei 变址寄存器
- s 比例因子,必须是 1、2、4或8
有效地址计算公式为:Imm + R[Eb] + R[Ei] * s 。
例:对于2(%esp,%eax,4)这个操作数来讲,它代表的是内存地址为2+%esp+4*%eax的存储器区域的值。
3.4.2 数据传送指令
数据传送指令:将数据从一个位置复制到另一个位置的指令。
|
|
|
|
|
|
|
|
|
|
mov
|
|
|
|
|
|
|
|
|
|
MOV指令:mov + 源操作数 + 目的操作数
|
|
|
|
|
注:x86-64限制,两个操作数不能都指向内存 |
|
|
|
|
移送数据命令MOVZ和MOVS,将较小的源值复制到较大的目的时使用
- MOVZ:把目的中剩余的字节填充为0
- MOVS:通过符号扩展来填充,把源操作的最高位进行复制(0/1)
- movsbq $0xAA %rax --> %rax=FF…FFAA 因为0xA=1010,最高位是1,所以将其他字节设为1
- movzbq $0xAA %rax --> %rax=00…00AA
- movz/movs + bw/bl/bq/lw/lq/wq
- 两种移动数据方式+(低存储单位)+(高存储单位)
- cltq:专用——%rax <-- 符号扩展(%eax)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
3.4.3 数据传送实例
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.4.4 压入和弹出栈数据
|
|
|
|
|
|
|
|
|
|
第三栏:执行完pushq后立刻执行popq
值0x123仍然会被保存在内存位置0x100,直到被覆盖 %rsp总是指向栈顶 |
|
|
|
|