CSPP学习笔记-Ch3.2 程序编码

3.2 程序编码

写在前面:ATT 和 Intel 汇编代码格式
CSPP 的表述是基于 ATT 的(根据“AT&T”命名的,AT&T是运营贝尔实验室多年的公司)格式的汇编代码,这是 GCC、OBJDUMP 和其他一些我们使用的工具的默认格式。
其他一些编程工具,包括 Microsoft 的工具,以及来自 Intel 的文档,其汇编代码都是 Intel 格式的。
——这两种格式在许多方面有所不同:
● Intel 代码省略了指示大小的后缀。我们看到指令 pushmov,而不是 pushqmovq
● 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 将源代码转化成可执行代码流程:

  1. C 预处理器扩展源代码,插入所有用 #include 命令指定的文件,并扩展所有用 #define 声明指定的宏;
  2. 编译器产生两个源文件的汇编代码,名字分别为 p1.s 和 p2.s;
  3. 汇编器会将汇编代码转化成二进制目标代码文件 p1.o 和 p2.o;
    目标代码是机器代码的一种形式,包含所有指令的二进制表示,但是还没有填入全局值的地址,无法运行。
  4. 链接器将两个目标代码文件与实现库函数的代码合并,并产生最终的可执行文件 p (由命令行指示符 -o p 指定的)。

3.2.1 机器级代码

两种重要抽象:

  1. 指令集体系结构或指令集架构(Instruction Set Architecture,ISA)来定义机器级程序的格式和行为,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
    处理器的硬件并发地执行许多指令,但可以采取措施保证整体行为与 ISA 指定的顺序执行的行为完全一致。
  2. 机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。

机器级代码和 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’,这些后缀是数据类型所占字节大小的指示符,大多数情况下可省略。相反,反汇编器给 callret 指令添加了后缀‘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 生成的文件反汇编得到的代码,其几乎一样,但有以下区别:

  1. 地址(offset)不同:链接器将这段代码的地址移到了一段不同的地址范围中;
  2. 链接器填上了 callq 指令调用函数 mult2 需要使用的地址(上述代码第5行);
    链接器的任务之一:为函数调用找到匹配的函数的可执行代码的位置。
  3. 最后多了两行代码(第9、10行),这些指令对程序没有影响,出现的原因是为了使函数代码变为 16 字节,使得存储器系统能更好地放置下一个代码块(字节对齐)。

3.2.3 结合 C 程序和汇编代码

C 语言无法访问一些机器特性,但有时候必须要访问:在C 语言中直接混合汇编代码对这些特性进行访问,可以提高效率!因此:只应该在想要的特性只能以此种方式才能访问时才使用它

C 程序插入汇编代码的两种方法:

  1. 用汇编代码编写完整的函数,放进一个独立的汇编代码文件中,让汇编器和链接器将其和C 语言书写的代码合并起来;
  2. 使用 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 的信息

猜你喜欢

转载自blog.csdn.net/Chauncyxu/article/details/121890222