Chapter 3: 程序的机器级表示¶
约 1218 个字 15 行代码 预计阅读时间 4 分钟
前言
我们如今编程都是写诸如 C, Python, Jave 之类的高级语言,而过去的程序员们是直接写汇编的,如今的编译器已经可以把高级语言翻译成汇编,那么学习汇编对我们来说似乎就没那么有必要了.
那为什么我们还要花时间来学习汇编语言呢?因为通过阅读汇编代码,我们可以理解编译器的优化能力,分析代码中隐藏的低效率问题,并尝试最大化一段代码的性能.同时,高级语言提供的抽象层会隐藏一些我们想要了解的细节,而汇编则可以帮助我们理解这些细节.
本章我们将基于 x86-84 架构学习:C语言,汇编代码以及机器码之间的关系;了解如何实现C语言的控制结构,函数调用,数组和指针等;最后将会学习一些使用 GDB 调试代码的技巧.
3.2 程序编码¶
3.2.1 机器级代码¶
计算机系统使用多种不同形式的抽象,利用更简单的抽象来隐藏实现的细节.对于机器级编程来说,有两种抽象尤为重要:
- 指令集体系结构或指令集架构(ISA)来定义机器级程序的格式和行为.它将程序的行为描述成好像每条指令都是按顺序执行的,一条结束后再执行下一条.处理器的硬件远比,它们并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序一致.
- 机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组.
一些对C语言程序员隐藏的处理器状态在机器级编程中是可见的:
- 程序计数器(PC,在x86-64中用%rip表示):给出将要执行的下一条指令在内存中的地址.
- 整数寄存器文件:包含16个命名的位置,分别存储64位的值.
- 条件码寄存器:保存最近执行的算术或逻辑指令的状态信息.它们用来实现控制或数据流中的条件变化,比如说用来实现
if
和while
语句. - 一组向量寄存器:用来存储一个或多个整数或浮点数值.
3.2.2 代码示例¶
机器执行的程序只是一个字节序列,它是对一系列编码的指令.机器对产生这些指令的源代码几乎一无所知.
一些关于机器代码和它的反汇编表示的特性值得注意:
- x86-64的指令长度从1到15个字节不等.常用的指令以及操作数较少的指令通常比较短.
- 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一地解码成机器指令.例如,只有指令
pushq %rbx
的第一个字节是0x53
. - 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码.它不需要访问该程序的源代码或汇编代码.
- 反汇编器的指令命名规则与 GCC 生成的汇编代码使用的有细微差别.例如,
movq
指令在反汇编器中被命名为mov
.
3.2.3 关于格式的注解¶
假设我们有如下代码:
C | |
---|---|
使用 GCC 生成汇编代码之后:
GAS | |
---|---|
我们发现有很多以'.'开头的行,这些行是汇编器的伪指令,用来控制汇编器的行为.我们通常可以忽略这些行.
所以我们采用这样一种格式来表示汇编代码,它省略了大部分伪指令,但包含行号和解释性说明,例:
void multisotre(long x, long y, long *dest) {
x in %rdi, y in %rsi, dest in %rdx
1 multisotre:
2 pushq %rbx Save %rbx
3 movq %rdx, %rbx Copy dest to %rbx
4 call mult2 Call mult2(x, y)
5 movq %rax, (%rbx) Store result at dest
6 popq %rbx Restore %rbx
7 ret Return
3.3 数据格式¶
C声明 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
---|---|---|---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
Question
我们观察到,在汇编代码指令中,都有一个字符后缀,而整数 int 和 双精度浮点数 double 都是用 ‘l’ 后缀,这样是否会产生歧义呢?
答案是不会,因为整数和浮点数使用的是完全不同的两组指令和寄存器。