AT&T汇编是学习Linux内核必备基础知识。Linux内核有大量AT&T汇编代码。本文介绍如何使用标准的AT&T语法编写x86汇编代码。
完整的x86指令集庞大而复杂(Intel的x86指令手册有几千页),这里不做全面覆盖。本系列文章将重点聚焦于x86编程的现代特性,仅深入讲解基础指令集以帮助读者建立基本认知。
寄存器
现代x86处理器(386及以上)包含8个32位通用寄存器如图1所示。寄存器名称大多具有历史渊源:EAX曾作为累加器用于算术运算,ECX因常用于存储循环计数而得名。虽然多数寄存器在现代指令集中已失去特殊用途,但按惯例仍有两个专用寄存器——栈指针(ESP)和基址指针(EBP)。
对于EAX、EBX、ECX和EDX寄存器,可访问其子寄存器。例如,EAX的低16位可作为AX寄存器使用,AX的低8位称为AL,高8位称为AH。这些名称指向同一物理寄存器:当向DX存入双字节数据时,会同时影响DH、DL和EDX的值。这些子寄存器主要是早期16位指令集的遗留设计,但在处理小于32位的数据(如单字节ASCII字符)时仍具实用价值。
内存与寻址模式
声明静态数据区
在x86汇编中,您可以通过专用的汇编器指令声明静态数据区(类似于全局变量)。数据声明需以.data指令开头。此后可用.byte、.short和.long指令分别声明单字节、双字节(16位)与四字节(32位)的数据存储空间。为引用这些数据的地址,可为其附加标签。标签在汇编中具有高度灵活性与实用性,它们为内存地址赋予名称,具体地址值将由汇编器或链接器后期解析。这种方式类似于高级语言里通过名称声明变量,但需遵循更底层的规则,例如连续声明的数据将在内存中相邻存储。
示例声明:
.data
var:
.byte 64 /* 申明一个字节,变量名var,值为64. */
.byte 10 /* 申明一个无名变量,值为 10. 存储位置是: var + 1. */
x:
.short 42 /* 申明一个变量名为x的两字节,值为 42*/
y:
.long 30000 /* 申明y变量,初始值为: 30000. */
与支持多维数组及索引访问的高级语言不同,x86汇编语言中的数组本质上是内存中连续分布的存储单元。声明数组只需罗列元素值即可,如下方第一个示例所示。对于字节数组的特殊场景,可直接使用字符串字面量进行初始化。若需在内存中开辟填充零值的区域,可使用.zero指令。
示例:
s:
.long 1, 2, 3 /* 申明3个4字节空间,初始值分别为: 1, 2和 3.以变量名s为开始位置。位置 s + 8 的值是 3. */
barr:
.zero 10 /* 在barr位置申请初始值为0的10个字节. */
str:
.string "hello" /* 在str位置申请6字节,初始值是字符串的ASCII码,并且以null(0)结尾 */
内存寻址
现代x86兼容处理器能够寻址高达2³²字节(4GB)的内存空间,其内存地址采用32位宽度。在前述示例中,通过标签引用的内存区域实际上会被汇编器替换为具体的32位内存地址。除通过标签(即常量地址值)访问内存外,x86架构提供了一种灵活的地址计算机制:最多可将两个32位寄存器与一个32位有符号常量相加来生成内存地址,其中一个寄存器还可选择预先乘以2、4或8的系数(用于处理不同数据宽度的偏移计算)。
这种寻址模式可应用于多数x86指令(具体指令将在后续章节详述)。此处我们以数据传送指令mov为例进行说明,该指令用于在寄存器与内存间传输数据,其语法包含两个操作数:第一个操作数为源操作数,第二个指定目标操作数。
以下是使用地址计算方式的mov指令示例:
mov (%ebx), %eax /* 将EBX寄存器指向的内存地址处的4字节数据加载到EAX */
mov %ebx, var(,1) /* 将EBX中的值存入内存地址var处的4字节空间(注意:var为32位常量地址)*/
mov -4(%esi), %eax /* 将ESI寄存器值减4后指向的内存地址的4字节数据加载到EAX */
mov %cl, (%esi,%eax,1) /* 将CL寄存器的值存入ESI+EAX计算得到的内存地址处的1字节空间 */
mov (%esi,%ebx,4), %edx /* 将ESI+4*EBX计算得到的内存地址处的4字节数据加载到EDX */
如下是非法地址计算示例:
mov (%ebx,%ecx,-1), %eax /*只允许累加寄存器. */
mov %ebx, (%eax,%esi,%edi,1) /* 地址计算时,最多有两个寄存器. */
操作符后缀
通常情况下,内存地址处数据项的预期大小可通过汇编代码指令的上下文推断。例如在上述所有指令中,内存区域的大小均可从寄存器操作数的位数推导:当加载32位寄存器时,汇编器可推断目标内存区域为4字节宽;当存储单字节寄存器值时,汇编器则推断目标地址指向内存中的单个字节。
但某些情况下,被引用内存区域的大小存在歧义。以指令mov $2, (%ebx)为例:该指令是将数值2存入EBX地址指向的单个字节,还是将32位整型值2存入从EBX地址起始的4字节空间?由于两种解释均可能成立,汇编器必须通过显式指定后缀来消除歧义。为此,x86汇编引入了b(1字节)、w(2字节,字)和l(4字节,长字)后缀来明确操作数大小。
示例:
movb $2, (%ebx) /* 将数值2存入EBX寄存器指向的单个字节内存(8位操作) */
movw $2, (%ebx) /* 将16位整型值2存入EBX指向的连续2字节内存(低字节在前) */
movl $2, (%ebx) /* 将32位整型值0x00000002存入EBX指向的4字节内存(小端存储) */