目录
1. 汇编语言和机器级语言
1.1 不同的编程语言
1.2 Linux下的汇编语言
2. 程序编码
1.1 机器级代码
1.2 代码示例
3. 数据格式
本文基于CSAPP第三章撰写,主要介绍部分x86-64汇编的相关知识,后续会将该部分内容慢慢完善(PS:所有代码编译和汇编均在Linux环境下进行)
1. 汇编语言和机器级语言
1.1 不同的编程语言
从计算机诞生至今,编程语言总数超过2500种编程语言,其发展大致经历了四个阶段
- 机器语言:是一种二进制语言, 由二进制数0和1组成的指令代码的集合,机器能直接识别和 执行,各种机器的指令系统互不相同
- 汇编语言:汇编指令是机器指令便于记忆和阅读的书写格式——助记符,与人类语言接近,add、mov、sub和call等
- 面向过程的高级语言:如C语言
- 面向对象的高级语言:如C++和Java
编程语言的发展简史——编年史
🔷 机器语言
在最早期采用穿孔纸带保存程序(1-打孔,0-不打孔)
优点:
1. 速度快 2. 占存储空间小 3. 翻译质量高缺点:
1. 可移植性差 2. 编译难度大 3. 直观性差 4. 调试困难
🔷 汇编语言
用助记符代替机器指令的操作码,用地址符号或标号代替指令或操数的地址
- 机器指令:1000100111011000
- 操 作:寄存器bx的内容送到ax中
- 汇编指令:mov %bx, %ax,
汇编指令同机器指令是一一对应的关系
优点:
1.执行速度快 2.占存储空间小 3.可读性有所提高缺点:
1.类似机器语言 2.可移植性差 3.与人类语言还相差很悬殊
🔷 高级语言
C++和Java等高级语言与汇编语言及机器语言之间是一对多的关系
一条简单的C++语句会被扩展成多条汇编语言或者机器语言指令
高级语言到机器语言的转换方法:
- 解释方式:通过解释程序,逐行转换成机器语言,转换一行运行一行
- 编译方式(翻译方式):通过编译程序(编译、链接)将整个程序转换成机器语言
汇编语言与高级语言的比较
应用程序类型 | 高级语言 | 汇编语言 |
用于单一平台的中到大型商业应用软件 | 正式的结构化支持使组织和维护大量代码很方便 | 最小的结构支持使程序员需要人工组织大量代码,使各种不同水平的程序员维护现存代码的难度极高 |
硬件驱动程序 | 语言本身未必提供直接访问硬件的能力,即使提供了也因为要经常使 用大量的技巧而导致维护困难 | 硬件访问简单直接。当程序很短并且文档齐全时很容易维护 |
多种平台下的商业应用软件 | 可移植性好,在不同平 台可以重新编译,需要 改动的源代码很少 | 必须为每种平台重新编写程序,通常要使用不同的汇编语言,难于维护 |
需要直接访问硬件的嵌入式系统和计算机游戏 | 由于生成的执行代码过 大,执行效率低 | 很理想,执行代码很小并且运行很快 |
1.2 Linux下的汇编语言
x86汇编语言通常有两种风格——AT&T汇编、Intel 汇编,其中DOS操作系统采用Intel汇编风格,而Linux下采用AT&T汇编风格
AT&T汇编 | Intel 汇编 | 说明 | |
寄存器前缀% | %eax | eax | Intel省略了寄存器前缀% |
源/目的操作数顺序 | movl %eax,%ebx | mov ebx,eax | AT&T源操作数在前,目的操作数在后,Intel反之 |
常数/立即数的格式$ | movl $0xd00d, %ebx | mov ebx,0xd00d | AT&T在立即数前加$ |
操作数长度标识 | movw var_x, %bx (b-1字节,w-2字节,l-4 字节,q-8 字节) | mov bx, word ptr var_x | Intel省略了指示大小的后缀 |
寻址方式 | imm32(basepointer, indexpointer,indexscale) | [basepointer+indexpointer *indexscale + imm32] | Linux寻址将在后面介绍 |
2. 程序编码
对两个C语言文件p1.c和p2.c进行编译的命令行为
gcc -Og -o p p1.c p2.c
- gcc:指GCC编译器
- Og:指明优化等级,-Og是基础优化项,告诉编译器使用生成符合原始C代码整体结构的机器代码的优化等级
- -o:后面接生成的可执行程序的名称(若不使用,默认生成a.out)
再来回顾一下程序的编译链接过程
- 预处理阶段,C预处理器扩展源代码,插入所有用#include命令指定的文件,并展开所有的宏定义
- 编译阶段,编译器生成两个源文件的汇编代码,名字分别为p1.s 和 p2.s
- 汇编阶段,汇编器将汇编代码转化成二进制目标文件p1.o 和 p2.o(此时所有指令均为二进制,但还没有填入全局值的地址)
- 链接阶段,链接器将两个目标文件与库函数合并,生成可执行代码文件p
2.1 机器级代码
对于机器级编程来说,有两种抽象尤为重要
- 第一种是指令集体系结构或指令集架构(Instruction Set Architecture,ISA),它定义了处理器状态、指令的格式以及每条指令对状态的影响
- 第二种是机器级程序使用的内存地址是虚拟地址,有关虚拟地址的内容以后将会介绍
在整个编译的过程中,编译器会完成大部分的工作,编译过后生成的汇编代码十分接近于机器代码,但与二进制的机器代码相比,汇编代码用文本格式表示,具有更好的可读性
x86-64的机器代码和原始的C代码差别很大,一些通常对C语言程序员隐藏的处理器状态都是可见的:
- 程序计数器(通常称为PC,在x86-64中用%rip表示):给出将要执行的下一条指令在内存中的地址
- 整数寄存器文件:包含16个命名位置,分别储存64位的值。这些寄存器可以储存地址或整数数据,有的被用来记录程序状态、有的用来保存临时数据例如参数和局部变量,函数返回值等
- 条件码寄存器:保存着最近执行的算术或逻辑指令的状态信息
- 向量寄存器:存放一个或多个整数或浮点数值
机器级代码将内存简单地看成一个很大的、按字节寻址的数组(这意味着它并不像C语言一样会将内存分为多个部分,它只会按照地址来处理数据)。对于数据类型,汇编代码并不区分有符号或无符号整数、不区分各种指针,甚至不区分指针和整数
一条机器指令只执行一个非常基本的操作。例如,将两个寄存器中的数值相加,或简单地在寄存器之间传送数据。然而,整个程序的构建正是由这一条条简单的指令所构成
2.2 代码示例
假设我们写了一个C语言程序mstore.c,包含如下函数定义
long mult2(long, long);
void multstore(long x, long y, long* dest)
{
long t = mult2(x, y);
*dest = t;
}
仅对其进行编译的命令行和生成的文件为
gcc -Og -S mstore.c
-rw-rw-r-- 1 hqs hqs 393 Jan 12 11:52 mstore.s
得到mstore.s文件,用cat将其打印到屏幕上可以看到原始C代码的汇编为
[hqs@VM-8-2-centos test]$ cat mstore.s
.file "mstore.c"
.text
.globl multstore
.type multstore, @function
multstore:
.LFB0:
.cfi_startproc
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE0:
.size multstore, .-multstore
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits
所有以‘.’开头的都是指导汇编器和链接器工作的伪指令,通常可以忽略。关于.cfi指令,有兴趣可以查看CFI directives (Using as) ,这里不对进行展开
上述汇编代码中最主要的几行如下,每条指令都对应一条机器级指令
multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
接下来我们使用mstore.c文件汇编后生成的mstore.o文件,所使用的命令行和生成的文件为
gcc -Og -c mstore.c
-rw-rw-r-- 1 hqs hqs 1368 Jan 12 12:06 mstore.o
使用gdb对这段代码进行调试,使用如下的命令行可以展示函数multstore的二进制代码
[hqs@VM-8-2-centos test]$ gdb mstore.o
(gdb) x/14xb multstore
这条命令告诉GDB显示(简写为‘x’)从函数multstore开始位置往后的14字节(简写为'b')内容的十六进制表示(简写为'x')
得到的结果为如下
(gdb) x/14xb multstore
0x0 <multstore>: 0x53 0x48 0x89 0xd3 0xe8 0x00 0x00 0x00
0x8 <multstore+8>: 0x00 0x48 0x89 0x03 0x5b 0xc3
但是仅仅这样并没有很好地观察到汇编代码和二进制机器代码的对应关系,这时使用objdump -d命令完成对于.o文件的反汇编
objdump -d:
- 检查目标代码的有用工具
- 分析指令的位模式
- 生成近似的汇编代码表述/译文
- 可处理a.out (完整可执行文件)或 .o文件
可以看到下面的每一条汇编指令都存在一条相应的二进制指令,比如 push %rbx对应的二进制指令为53,mov %rdx %rbx对应的二进制指令为 48 89 d3
[hqs@VM-8-2-centos test]$ objdump -d mstore.o
mstore.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <multstore>:
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
Disassembly of section .text 表示这是.text节的反汇编,也就是程序的代码段的产生的汇编
最左边的一行表示这条指令的相对于.text段起始地址的偏移量,也就是相对于程序第一条代码的偏移量,可以理解为相对位置,因此最后一条指令的偏移量为十六进制数d,也就是13,而指令长度为1个字节,因此这段程序的机器级代码总长度为14字节
机器级代码和它的反汇编表示有一些值得注意的地方
- x86-64的指令长度为1-15字节不等。常用指令字节数少,不常用指令字节数多
- 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一的解码成机器指令。比如说遇到53,便知道是push指令的开头;遇到48,知道是mov指令的开头
- 反汇编器只是基于机器代码文件中的字节序来确定汇编代码,不访问程序的源代码或汇编代码
- 反汇编器的命名规则和GCC生成的汇编有一些差异,比如说省略操作数大小指示符
接下来我们创建一个完整的工程,添加一个main函数,其中包括mult2函数的定义
#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;
}
用下面的命令生成可执行程序prog
gcc -Og -o prog main.c mstore.c
-rwxrwxr-x 1 hqs hqs 8456 Jan 12 13:49 prog
同样用objdump -d查看prog文件的反汇编
000000000040056b <multstore>:
40056b: 53 push %rbx
40056c: 48 89 d3 mov %rdx,%rbx
40056f: e8 ef ff ff ff callq 400563 <mult2>
400574: 48 89 03 mov %rax,(%rbx)
400577: 5b pop %rbx
400578: c3 retq
这时prog文件相比于之前的mstore.o文件,经过了链接的过程,所有的指令都带上了地址,而不再是相对于起始位置的偏移了,之后在链接部分会介绍可重定位目标文件和可执行目标文件文件的差别
3. 数据格式
由于是从16位体系结构扩展为32位的,Intel用术语“字(word)”来表示16位数据类型
在x86-64指令集中,各种数据类型之间的关系为
C声明 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
大多数GCC生成的汇编代码指令都有一个字符的后缀,表示操作数的大小,如
movq %rdx, %rbx
mov是原始指令,后面跟了一个q,说明在两个寄存器之间操作的数是四字类型的,在C语言中可表示long或者指针。因此,一条指令可能有多个变种,mov指令可能有movb、movw、movl和movq四种类型
值得注意的是,汇编代码也使用后缀 'l' 来表示四字节整数和双精度浮点数,由于前者使用整数寄存器,后者使用浮点寄存器,所有并不会产生歧义