目录
汇编语言基础
汇编程序基本由 4 种类型的组件组成:指令(instruction)
、伪指令(directive
)、标号(label)
以及注释(comment)
类型 | 示例 | 含义 |
---|---|---|
指令 | mov eax 0 |
给 EAX 赋值为0 |
伪指令 | .section.text |
以下代码放入 text 节 |
伪指令 | .string "foobar" |
定义一个 ASCII 字符串 foobar |
伪指令 | .long 0x12345678 |
定义一个双字 0x12345678 |
标号 | foo:.string "foobar" |
使用符号定义的 foobar 字符串 |
注释 | #注释 | 表示注释信息 |
x86 体系的寄存器说明
在 x86 架构的 CPU 中,存在多种可编程的寄存器,它们用于不同的目的,以下是一些主要的可编程寄存器类型:
- 通用寄存器 :
AX
(累加器寄存器):用于算术运算和 I/O 指令。BX
(基址寄存器):用于存储数据指针。CX
(计数寄存器):通常用于循环计数。DX
(数据寄存器):用于 I/O 操作和其他数据传输。SI
(源索引寄存器)、DI
(目的索引寄存器):用于字符串操作。BP
(基址指针寄存器):用于访问栈中的参数和局部变量。SP
(堆栈指针寄存器):用于管理堆栈。AX
、BX
、CX
和DX
寄存器还可以分为高字节和低字节寄存器,例如AH
和AL
分别是AX
的高字节和低字节。
EAX
是一个32位的寄存器,用于处理 32 位的数据,而 AX 是一个 16 位的寄存器,用于处理 16 位的数据。
在32位模式下,AX
是 EAX
的一个低位部分,可以直接用于 16 位数据的操作。
x86
基于原始的 8086 指令集,寄存器是16位宽,而32bit 的 x86 ISA
将这些寄存区扩展为32位,然后 x86-64
将它们进一步扩展为64位。
为了保证向后兼容性,新指令集中使用的寄存器是旧的寄存器的超集。
在汇编中指定寄存器的操作数,需要是以寄存器的名称,比如 mov rax,64
就是将 64 赋值给 rax 寄存器,
下图显示了如何将 x86-64 rax 寄存器分为传统的 32 位和 64 位寄存器,rax
的低32位形成了一个名位 eax
的寄存器,
其低16位形成了一个原始的 8086 寄存器 AX,读者可以通过寄存器名称 AL 访问 AX 的低字节,通过 AH 访问 AX 的高字节
同样的,其他的通用寄存器,也会与类似的用法:
描述 | 64bit | 低32bit | 0~16 bit | 8~16 bit | 0~8 bit |
---|---|---|---|---|---|
累加器 | rxa | eax | ax | ah | al |
基址寄存器 | rbx | ebx | bx | bl | bh |
程序计数器 | rcx | ecx | cx | cl | ch |
数据寄存器 | rdx | edx | dx | dl | dh |
-
段寄存器:
CS
(代码段寄存器):包含当前执行指令的段地址。DS
(数据段寄存器):包含数据段的段地址。ES
(附加段寄存器):用于额外的数据段。FS
和GS
:提供额外的段寄存器,由操作系统或应用程序定义。SS
(堆栈段寄存器):包含堆栈段的段地址。
-
指令指针寄存器:
IP
(指令指针寄存器):也称为EIP
(在 32 位模式下)或RIP
(在 64 位模式下),它指向下一条要执行的指令。
本质上就是指针指针寄存器
-
标志寄存器:
FLAGS
(标志寄存器):包含各种状态标志,如进位、零、符号、溢出等。
-
控制寄存器:
CR0
-CR4
:用于控制处理器的操作模式和特性,如分页、保护模式等。
-
系统寄存器:
MSR
(模型特定寄存器):提供对处理器特定功能的访问,如性能监控计数器。
-
浮点寄存器:
ST0
-ST7
(浮点堆栈寄存器):用于浮点运算。Control Word
、Status Word
、Tag Word
、Instruction Pointer
和Last Instruction Opcode
:用于控制浮点单元的操作和报告状态。
-
SIMD 寄存器:
MMX
寄存器:用于多媒体扩展运算。XMM0
-XMM15
(SSE 寄存器):用于 SIMD 扩展运算。YMM0
-YMM15
(AVX 寄存器):在 AVX 指令集中使用,提供更大的 SIMD 数据宽度。
这些寄存器在不同的 x86 处理器模式下(实模式、保护模式、长模式等)可能会有不同的名称和大小。
例如,在 32 位模式下,通用寄存器通常有 32 位宽,而在 64 位模式下,它们会扩展到 64 位宽。
Intel 和 AT&T 语法
正如前面所提到的,不同的汇编器对汇编程序有不同的语法,表示 x86
机器指令的语法格式主要有两种:Intel
语法 和 AT&T
语法,
AT&T 语法显式的再每个寄存器名称前面加上 % 符号,每个常量前面加上$ 符号, Intel 语法没有这种格式:
AT&T
和 Intel
之间最重要的区别是使用完全相反的指令操作数顺序:
在AT&T语法中源操作数在目的操作数前面,所以是从左向右读
在Intel语法中目的操作数位于源操作数前面,所以是从右向左读
如果汇编语言中出现 % 和 $ 前缀,就是使用AT&T语法,应该从左向右读
"helloworld" 汇编程序分析
helloworld
的汇编代码:
c
1 .file "main.c"
2 .text
3 .section .rodata
4 .LC0:
5 .string "hello world"
6 .text
7 .globl main
8 .type main, @function
9 main:
10 .LFB0:
11 .cfi_startproc
12 endbr64
13 pushq %rbp
14 .cfi_def_cfa_offset 16
15 .cfi_offset 6, -16
16 movq %rsp, %rbp
17 .cfi_def_cfa_register 6
18 leaq .LC0(%rip), %rdi
19 call puts@PLT
20 movl $0, %eax
21 popq %rbp
22 .cfi_def_cfa 7, 8
23 ret
24 .cfi_endproc
25 .LFE0:
26 .size main, .-main
27 .ident "GCC: (Ubuntu 9.5.0-1ubuntu1~22.04) 9.5.0"
28 .section .note.GNU-stack,"",@progbits
29 .section .note.gnu.property,"a"
30 .align 8
31 .long 1f - 0f
32 .long 4f - 1f
33 .long 5
34 0:
35 .string "GNU"
36 1:
37 .align 8
38 .long 0xc0000002
39 .long 3f - 2f
40 2:
41 .long 0x3
42 3:
43 .align 8
44 4:
从中可以看出:
printf
函数最终是调用到puts
执行打印helloworld
的操作- main 函数的返回值是通过
EAX
传递的 - 函数执行时会将当前
rbp
寄存器的值压入到堆栈中,然后将当前开辟的rsp
的值赋给它 - 函数执行结束会弹出
rbp
寄存器的值
指令和伪指令
指令
是 CPU 执行的实际操作,伪指令
意在告诉汇编工具生成特定的数据,并将指令和数据放在指定的节,
标号
是汇编工具中引用指令或数据的符号名称,注释
是可读注释。在程序被汇编链接成二进制程序后,所有的符号名称都被地址所取代
伪指令:
.section
告诉汇编工具将在那个节放置后面的内容,
.ascii
表示定义 ascii 字符串的伪指令,当前还有一些伪指令用于定义其他数据类型:
.byte
一个字节
.word
两个字节
.long
四个字节
.quad
八个字节