程序的机器级表示
这一章会描述一下代码的汇编表示。
计算机执行机器代码,用字节序列编码低级的操作,包括处理数据,管理内存,读写存储设备
上的数据,以及利用网络通信。
GCC C语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示。
编译器选项可以生成不同的汇编代码,影响着程序的运行效率。
这章的目的在于: 了解典型的编译器在将C语言程序结构变换成机器代码时候所做的转换。
优化编译器能够,重新排列执行顺序,消除不必要的计算,用快速操作替换慢速操作,
甚至将递归计算变换成迭代计算。重点在这几句,跟着书本继续往下看。
3.1 历史观点
说明了一下处理器迭代的进程,其晶体管的数量每年以百分之三十七的速度增长。
3.2程序编译
linux > gcc -Og(编译选项设定优化等级) -o p(生成的目标文件) p1.c P2.c (源文件)
这个指令调用了一整套程序,将源代码转化为可执行代码。
跟第一章预编译 编译 汇编 链接 对着理解。
3.2.1机器级代码
指令集体系结构或指令集架构是定义机器级程序的格式和行为的一种抽象模型。
另外一个很重要的抽象是 机器级程序使用的内存地址都是虚拟地址,提供的内存模型看上去
是一个非常大的字节数组。
PC计数器:给出将要执行的下一条指令在内存中的地址。
整数寄存器文件:包含16个命名的位置,存储64位信息,可存储地址和整数信息。部分可存储临时信息。
条件码寄存器:存储最近执行的算数或者逻辑执行的状态信息。 if while 之类的改变数据流的方向。
一组向量寄存器:一个或者多个整数或者浮点值。
操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器内存中的物理地址。
3.2.2 代码示例
long mult2(long , long);
void multstore(long x,long y,long *dest){
long t = mult2(x,y);
*dest = t;
}
linux > gcc - Og -S mstore.c 生成汇编代码
multstore:
pushq %rbx
movq %rdx,%rbx
call mult2
movq %rax,(%rbx)
popq %rbx
ret
下面是详细和对照注释:
pushq %rbx: 将%rbx寄存器的值压入栈中,保存现场。
movq %rdx, %rbx: 将%rdx寄存器中的值复制到%rbx寄存器中,相当于将%rbx寄存器保存了%rdx的值。
call mult2: 调用mult2函数,该函数的功能是将传入的参数乘以2,并将结果存储在%rax寄存器中。
movq %rax, (%rbx): 将%rax寄存器中的值(即乘以2后的结果)存储到%rbx寄存器指向的地址中,即将乘以2后的结果存储到原%rdx寄存器中的值所指向的地址中。
popq %rbx: 弹出栈顶的值,将之前保存的%rbx寄存器的值恢复,恢复现场。
ret: 函数返回。
对照:
C函数long mult2(long a, long b);对应汇编代码中的call mult2,表示调用mult2函数。
C函数void multstore(long x, long y, long *dest)对应汇编代码中的multstore函数。
C代码中的long t = mult2(x, y);对应汇编代码中的call mult2和movq %rax, (%rbx),表示将mult2函数的返回值存储到t变量中。 %rax = t
C代码中的*dest = t;对应汇编代码中的movq %rax, (%rbx),表示将变量t的值存储到dest指向的地址中。 (%rbx) 带()指的是取地址。
linux > gcc -Og -c mstore.c 生成目标文件代码 二进制格式的
53 48 89 d3 e8 … 一共14个字节
既机器执行的程序只是一个字节序列,它是对一系列指令的编码。
(gdb) x/14xb multstore GUN上查看二进制目标代码 从函数multstore 所处的地址14个地址字节处开始
linux> objdump -d mstore.o
可以通过反汇编器 生成一个 二进制目标代码对照的 汇编代码:
这个对照代码自己看书,东西挺多的。
链接器的任务之一就是为函数找到匹配的函数的可执行代码的位置。
C语言中插入汇编:
- 编写完整的函数,放入一个独立的汇编文件中,用汇编器和链接器把它和C语言合并。
2.GCC的内联汇编特性,用asm伪指令可以在C语言中插入简短的汇编代码。
3.3数据格式
第一开始都是从16体系架构扩展的,16-32-64,术语 字 就从 32 双字 64 四字。
64位机器之下
数据类型 汇编代码后缀
char b
short w
int l
long q
char * q
float s 看浮点数有一点不符合规律,是因为浮点数用的寄存器不一样
double l
数据传送有四个变种 movb 传送字节 movw 传送字 movl传送双字 movq 传送四字
3.4访问信息
CPU的中央处理单元,包含一组16个存储64位值得 通用目的寄存器。以%r 开头
给了一个表 分四组根据数据大小划分举个例子
通用的有被调用者保存 调用者保存 参数寄存器 栈指针 返回值 这些类别寄存器
63 31 15 7 0
%rax %eax %ax %al //返回值
%rbx .... //被调用者保存
%rcx %rdx %rsi %rdi //参数寄存器
%rbp %r12 ~%r15 //被调用者保存
%rsp .... // 栈指针
%r10 %r11 调用者保存
3.4.1 操作指示符
大多数指令有一个或者多个操作数,指示出执行一个指令所需要的源操作数和放置结果的目的的位置。
操作数类型
立即数:$566 表示常数值
寄存器:r_a 来表示任意寄存器a ,R[r_a] 来表示寄存器a的值 (下划线是代替)
内存引用:他会根据计算出来的地址访问某个内存位置
M_b(Addr)表示对存储在Addr内存地址中 b个字节值的引用
给了一个表自己看原表理解下
$Imm Imm 立即数寻址
r_a R[r_a] 寄存器寻址
Imm M[Imm] 绝对寻址
(r_a) M[R[r_a]] 间接寻址 //这个就是指针典型了
还有基址+偏移量 C语言中的数据结构访问其元素值
变址寻址
比例变址寻址
3.4.2 数据传送指令
mov 将源操作数 复制到 目的位置
源操作数指定的值是一个立即数,存储在寄存器或者内存中。
目的操作数指定一个位置,要么是一个寄存器或者内存地址。
X86有一个限制,两个操作数不能都指向内存地址。
将一个值从一个内存位置复制到另外一个内存位置需要两条指令。
一个指令将源值加载到寄存器中,第二天将该寄存器的值写入目的位置。
这想要细节有很多需要自己看书,书上细节有些多,精力有限无法一一陈列。
movzbw 零扩展字节传送到字
movsbw 符号扩展字节传送到字
3.4.3 数据传送示例
long exchange(long *xp, long y)
{
long x = *xp;
*xp = y;
return x ;
}
exchange :
movq (%rdi),%rax // (%rdi) 这就是间接寻址,代指获取指针所指向的地址值
movq %rsi,(%rdi) // 将y的值赋给 xp所指向的地址
ret
C语言中所谓的指针其实就是地址,间接引用(*)指针 就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器。内存引用就是 寄存器中有一个地址值,这个地址里面存放着实际的数据,获取的就是这个实际的数据。
看3.4.1 操作指示符 中的间接寻址和绝对寻址
指针值就是 Imm = R[r_a] , 转了一下 所以也可以很直观理解为什么叫间接寻址了。
x 这类局部变量一般是保存在寄存器中。
C操作符 & 代表 取址,创建一个指针。
long a = 4
exchange(&a,3); 这个指针指向保存局部变量a的位置。
3.4.4 压入和弹出栈数据
pushq S 将四字压入程序栈中 入栈是地址减去8字节 因为栈是向低地址方向增长
popq D 将四字弹出程序栈 出栈则是当前地址+8
栈有后入先出的特征
3.5 算数和逻辑操作
几类操作: 加载有效地址,一元操作,二元操作,和移位。
leaq 加载有效的地址 将有效地址写到目的操作数
inc 自加1 DEC自减1 NEG取反 NOT 取补
二元操作 加减乘除与或 ADD SUB IMUL XOR OR AND
左移右移SAL SHL SAR SHR 算术右移 逻辑右移 后缀 sabl 代表操作数据的大小、
有个规律 操作符后缀是w b l 是根据目的操作数来算的
3.5.5 特殊的算数操作
就是说了当计算结果大于64位的时候,以128位为例 ,用%rdx 高64位 %rax 低64位
3.6控制
前面只是说了直线代码的行为,现在加了控制就可以有其他线。
3.6.1条件码
CF 进位
ZF 零标志
SF 符号标志
OF 溢出标志
cmpb cmpw cmpl cmpq 和 test b w l q 只设置条件码而不改变任何其他寄存器
3.6.2 访问条件码
set 类指令 访问条件码
jmp 无条件跳转 ,j 加后缀有不同的效果。可参考书上的图
3.6.4跳转指令的编码
jmp .L2
.L3:
XXXX
.L2
test %rax,%rax 判断条件
jmp .L3
3.6.6用条件传送来实现条件分支
说了一下条件传送和条件控制之间的差别主要是性能影响,多用条件传送,
这个都是由于计算机有一个分支预测系统提前预测可能的分支结果并计算,预算错误有一个乘法时间。
而条件传送计算对和错两条分支的结果,根据最后的判断来赋值最终对的结果,而不是条件控制先判断在计算对的分支结果在赋值。
3.6.7 循环
whlie for 这一类都是向上面if else 那样 jmp .L3那样。
3.6.8 switch
它稍微特别了一点维护了一个 跳转表,搞了一个跳转数组对应他们的case 整数值。
3.7过程
传递控制 A调用B 必须把程序计数器指向B的起始地址
传递数据 A传递的参数,A自己的数据,A的返回地址,B的返回值。
分配和释放内存 B开始为它的数据分配空间,B释放时为它的数据清除空间
3.7.1 运行时栈 和数据传送
通过寄存器最多传送6个参数,后面就不是通过寄存器存储参数,取值会变慢,影响性能
A 调用B的时候,一般要保存自己的数据到一类寄存器中,实在保存不了,有机质让B保存,
B不能覆盖掉这一类寄存器的值。 返回时需要弹出这些值,包括返回地址值。
3.7.2转移控制
call Label 栈指针 %rsp %rip 程序计数器 就是pc程序计数器 的地址的更新,栈指针指向的是栈顶地址
3.7.4 栈上的局部存储
局部数据必须存放在内存中
寄存器不足够存放所有的本地数据
对一个局部变量使用&
某些局部变量是数组或者结构。
172 那个地址小图可以看一下
3.7.5 寄存器中的局部存储空间
%rbx %rbp %r12~%r15 被调用者保存寄存器 A的数据存储在这些寄存器中,B不能修改,B退出时弹出这些数据。
调用者保存寄存器 B可以随意修改
3.7.6 递归过程
递归调用一个函数,每次函数调用都有它自己私有的状态信息。
3.8数组的分配和访问
数组的名字是其首地址
3.8.1基本原则
举了几个例子说了一下指针元素地址的大小计算。 X + L*i L是其数据类型的大小字节,x 为起始地址
指针大小随着计算机的位数不同而不同32 4字节 64 8字节
3.8.2 指针运算
&产生指针 * 间接引用指针
数组引用 A[i] = *(A+i) A是首地址 * (A首地址+ L类型大小地址* i)
3.8.3 嵌套的数组
T D[R][C] 把它们理解为一个行地址一个列地址, R为行 C为列就行了
3.8.4 定长数组
#define N 16
typedef int fix_maxrix[N][N] 这一类定长数组用宏定义好维护
3.8.5 变长数组
变长数组,就是根据传递的参数 变长。变长数组计算地址必须用乘法不能用移位
3.9 异质的数据结构
结构体struct union enum
3.9.1 结构
结构体其实也是一种对数据的小抽象在C的层次。 由于知道首地址,由于知道自己结构体的固定大小,所以可以通过元素地址的偏移来访问对应的元素。
3.9.2 联合
联合体就是其中的数据元素共用同一块内存,其所占内存大小取决于其中元素那个占最大,比如只有int A ;char B 那么就占4字节。
3.9.3. 数据对齐
许多计算机系统对基本数据结构的合法地址做出了一些限制,要求某种数据类型的对象的地址必须是某个K值得倍速也就是2 ,4, 8 等
假设处理器总是从内存读取8个字节,那么地址必须是8的倍数。
struct S1{
int i;
char c;
int j;
};如果是四字节对齐,他所占内存大小不是9 而是12,中间的char 补全了三字节
3.10.1 理解指针
每个指针都有一个值。这个值是某个指定类型的对象的地址。特殊的NULL 0 值表示指针没有指向任何地方。
指针用& 创建。
将指针从一种类型强制转化为另外一种类型,只改变了它的类型,并没有改变其值。
前面我说的强制转化你理解了就很好理解这句话。
指针也可以指向函数,函数指针的值是该函数机器代码表示中的第一条指令的地址。
int (*f)(int * ) 函数指针,它是一个指针,指向一个函数的入口地址。()括号必须要带
否则就会变成 (int * )f(int *);
3.10.2 使用GDB调试器
linux > gdb prog 可以获取prog的反汇编版本
3.10.3 内存越界引用和缓冲区溢出
C对于数组引用不进行任何的边界检查,而且局部变量和状态信息,都放在栈中。这两种情况结合到一起就能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。前面的A调用B的过程,有一个栈上返回地址的分配,然后在是B相关数据的分配,越界访问的时候,有概率访问到返回地址,如果修改了返回地址就出大问题了。黑客什么病毒什么就是这种原理
3.10.4 对抗缓冲区溢出攻击
栈随机化,地址空间布局随机化,程序开始时随机分配一个0·n直接的随机空间,让函数地址什么的随机化,就不好找到返回地址了,但是有概率暴力破解。
栈破坏检测
搞了一个金丝雀值检测,读取返回地址前,有一个金丝雀值,这个值没有被破坏的话,在读取返回地址时候会和一个存储金丝雀值得寄存器中得值对比,一致则继续读取返回地址。
3.10.5 支持变长栈帧
申请栈地址有随机化,用了一个%rbp 为帧指针放到返回地址的正下面,释放得时候将栈指针设置为%rbp得值,就可以直接弹出
3.11浮点代码
YMM 和xmm系列为浮点数的寄存器。