目录
3.2 程序编码
写在前面:ATT 和 Intel 汇编代码格式
CSPP 的表述是基于 ATT 的(根据“AT&T”命名的,AT&T是运营贝尔实验室多年的公司)格式的汇编代码,这是 GCC、OBJDUMP 和其他一些我们使用的工具的默认格式。
其他一些编程工具,包括 Microsoft 的工具,以及来自 Intel 的文档,其汇编代码都是 Intel 格式的。
——这两种格式在许多方面有所不同:
● Intel 代码省略了指示大小的后缀。我们看到指令 push
和 mov
,而不是 pushq
和 movq
。
● Intel 代码省略了寄存器名字前面的 %
符号,用的是 rbx
, 而不是 %rbx
。
● Intel 代码用不同的方式来描述内存中的位置,例如是 QWORD PTR [rbx]
而不是 (rbx)
。
● 在带有多个操作数的指令情况下,列出操作数的顺序相反。当在两种格式之间进行转换的时候,这一点非常令人困惑(确实很多人吐糟)。
# ATT 格式
multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
# 使用下述命令行,GCC 可以产生 multstore 函数的 Intel 格式的代码:
linux> gcc -Og -o p p1.c p2.c
multstore:
push rbx
mov rbx, rdx
call mult2
mov QWORD PTR [rbx], rax
pop rbx
ret
编译选项 -Og
告诉编译器使用会生成符合原始 C 代码整体结构的机器代码的优化等级。
gcc 将源代码转化成可执行代码流程:
- C 预处理器扩展源代码,插入所有用 #include 命令指定的文件,并扩展所有用 #define 声明指定的宏;
- 编译器产生两个源文件的汇编代码,名字分别为 p1.s 和 p2.s;
- 汇编器会将汇编代码转化成二进制目标代码文件 p1.o 和 p2.o;
目标代码是机器代码的一种形式,包含所有指令的二进制表示,但是还没有填入全局值的地址,无法运行。 - 链接器将两个目标代码文件与实现库函数的代码合并,并产生最终的可执行文件 p (由命令行指示符 -o p 指定的)。
3.2.1 机器级代码
两种重要抽象:
- 由指令集体系结构或指令集架构(Instruction Set Architecture,ISA)来定义机器级程序的格式和行为,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
处理器的硬件并发地执行许多指令,但可以采取措施保证整体行为与 ISA 指定的顺序执行的行为完全一致。 - 机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。
机器级代码和 C 语言的区别:
1、对 C 语言隐藏的处理器状态,对于x86-64 的机器代码都是可见的:
程序计数器
(通常称为“PC”,在 x86-64 中用 %rip 表示)给出将要执行的下一条指令在内存中的地址;- 整数
寄存器文件
包含 16 个命名的位置,分别存储 64 位的值。- 有的用来记录某些重要的程序状态;
- 有的用来存储地址(对应于C语言的指针);
- 有的用来保存临时数据,如过程参数、局部变量、函数返回值等。
条件码
寄存器保存着最近执行的算术或逻辑指令的状态信息,用来实现控制或数据流中的条件变化,如实现 if 和 while 语句。- 一组向量寄存器可以存放一个或多个整数或浮点数值。
2、对内存的理解
C 语言可以在内存中声明和分配各种数据类型的对象。
机器代码仅仅将内存视为一个很大的、按字节寻址的数组。
汇编代码都不区分有符号或无符号数,都不区分各种类型的指针,甚至不区分指针和整数。
机器指令
一条机器指令只执行一个非常基本的操作:
1、将存放在寄存器中的两个数字相加;
2、在存储器和寄存器之间传送数据;
3、条件分支转移到新的指令地址。
编译器负责产生这些指令的序列,从而实现程序结构。
机器指令最终也只是一个字节序列,是对一系列指令的编码。
3.2.2 代码示例
源代码:
/* mstore.c */
long mult2(long, long);
void multstore(long x, long y, long *dest){
long t = mult2(x,y);
*dest = t;
}
GCC 产生的汇编代码难读:
linux>> gcc -Og -S mstore.c
- 产生一个汇编文件 mstore.s ,包含一些不需要关心的信息,比如所有 ‘.’ 开头的行都是指导汇编器和链接器工作的伪指令。
- 不提供任何程序注释。
/* mstore.s */
.file "mstore.c"
.text
.globl multstore
.type multstore, @function
multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
.size multstore, .-multstore
.ident "GCC: (Ubuntu 4.8.1-2ubuntu1-12.04) 4.8.1"
.section .note.GNU-stack,"",@progbits
反汇编
对生成的机器代码进行反汇编。
反汇编器(disassembler),在Linux 系统中,带‘-d’ 命令行标志的程序 OBJDUMP(表示“object dump”)可以充当这个角色。
linux> objdump -d mstore.o
结果如下:(注解是人为后增加的)
0000000000000000 <multstore>:
#offset Bytes Equivalent assembly language
0: 53 push %rbx
1: 48 89 d3 mov %rdx,%rbx
4: e8 00 00 00 00 callq 9 <multstore+0x9>
9: 48 89 03 mov %rax,(%rbx)
c: 5b pop %rbx
d: c3 retq
关于机器代码及其反汇编表示的特性:
- x86-64 的指令长度从1到15个字节不等。
- 设计指令格式的方式是:从某个给定位置开始,可以将字节唯一地解码成机器指令。
例如:只有指令pushq %rbx
是以字节值 53 开头的。 - 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码。
它不需要访问该程序的源代码或汇编代码。 - 反汇编器使用的指令命名规则与 GCC 生成的汇编代码使用的有些细微的差别。
如省略了很多指令结尾的‘q’,这些后缀是数据类型所占字节大小的指示符,大多数情况下可省略。相反,反汇编器给call
和ret
指令添加了后缀‘q’,同样添加或省略也没有问题。
逆向工程循环
理解产生的汇编代码和源代码之间的关系,关键是找到程序值和寄存器之间的映射关系。
C语言编译器常常会重组计算
链接
生成实际可执行的代码 需要一组目标代码文件运行链接器,而这一组目标代码文件中必须含有一个 main 函数。假设
/* main.c */
#include<stdio.h>
void multstore(long, long, long*);
int main(){
long d;
multstore(2,3,&d);
printf("2*3 --> %ld\n",d);
return 0;
}
long mult2(long a, long b){
long s = a*b;
return s;
}
linux> gcc -Og -o prog main.c mstore.c
生成的 prog
文件变成了 8655 个字节,因为其不仅包括了 main.c 和 mstore.c 的代码,还包含了用来启动和终止程序的代码,以及用来与操作系统交互的代码。
反汇编 prog.o 文件
linux> objdump -d prog
0000000000400540 <multstore>:
#offset Bytes Equivalent assembly language
400540: 53 push %rbx
400541: 48 89 d3 mov %rdx,%rbx
400544: e8 42 00 00 00 callq 40058b <mult2>
400549: 48 89 03 mov %rax,(%rbx)
40054c: 5b pop %rbx
40054d: c3 retq
40054e: 90 nop
40054f: 90 nop
相比于对 mstore.c 生成的文件反汇编得到的代码,其几乎一样,但有以下区别:
- 地址(offset)不同:链接器将这段代码的地址移到了一段不同的地址范围中;
- 链接器填上了
callq
指令调用函数mult2
需要使用的地址(上述代码第5行);
链接器的任务之一:为函数调用找到匹配的函数的可执行代码的位置。 - 最后多了两行代码(第9、10行),这些指令对程序没有影响,出现的原因是为了使函数代码变为 16 字节,使得存储器系统能更好地放置下一个代码块(字节对齐)。
3.2.3 结合 C 程序和汇编代码
C 语言无法访问一些机器特性,但有时候必须要访问:在C 语言中直接混合汇编代码对这些特性进行访问,可以提高效率!因此:只应该在想要的特性只能以此种方式才能访问时才使用它。
C 程序插入汇编代码的两种方法:
- 用汇编代码编写完整的函数,放进一个独立的汇编代码文件中,让汇编器和链接器将其和C 语言书写的代码合并起来;
- 使用 GCC 的内联汇编特性,用
asm
伪指令可以在C 程序中包含简短的汇编代码。
3.2.4 应用:GDB调试器
# 启动GDB
linux> gdb prog
【GDB 命令实例】
命令 | 效果 |
---|---|
开始和停止quit run kill |
退出 GDB 运行程序(在此给出命令行参数) 停止程序 |
断点break multstore break * 0x400540 delete 1 delete |
在函数 multstore 入口处设置断点 在地址 0x400540 处设置断点 删除断点 1 删除所有断点 |
执行stepi stepi 4 nexti continue finish |
执行 1 条指令 执行 4 条指令 类似于 stepi,但以函数调用为单位 继续执行 运行到当前函数返回 |
检查代码disas disas multstore disas 0x400544 disas 0x400540, 0x40054d print /x $rip |
反汇编当前函数 反汇编函数 multstore 反汇编位于地址 0x400544 附近的函数 反汇编指定地址范围内的代码 以十六进制输出程序计数器的值 |
检查数据print $rax print /x $rax print /t $rax print 0x100 print /x 555 print /x ($rsp+8) print *(long *) 0x7fffffffe818 print *(long *) ($rsp+8) x/2g 0x7fffffffe818 x/20b multstore |
以十进制输出 %rax 的内容 以十六进制输出 %rax 的内容 以二进制输出 %rax 的内容 输出 0x100 的十进制表示 输出 555 的十六进制表示 以十六进制输出 %rsp 的内容加上 8 输出位于地址 0x7fffffffe818 的长整数 输出位于地址 %rsp+8 处的长整数 检查从地址 0x7fffffffe818 开始的双字 检查函数 multstore 的前 20 个字节 |
有用的信息info frame info registers help |
有关当前栈帧的信息 所有寄存器的值 获取有关 GDB 的信息 |