1 x86_64 汇编风格
x86_64
架构下的CPU
有两种主要的汇编风格: Intel
风格和AT&T
风格。
这里风格
是指汇编代码的书写语法,它们通过汇编器汇编生成的二进制指令是一样的,因为最终都要在x86_64
架构下的CPU
下运行。
支持Intel
汇编风格的操作系统主要是Windows
,支持AT&T
汇编风格的操作系统主要是Unix
以及Unix-like
操作系统,这主要是因为AT&T
贝尔实验室发明了Unix
操作系统。
本文主要介绍AT&T
汇编的基本语法。
2 AT&T 汇编
2.1 语法格式
AT&T
汇编的基本语法格式如下:
c
// AT&T 汇编风格
mnemonic source, destination
mnemonic
是助记符,比如mov
。
source
是源操作数。
destination
是目的操作数。
从语法上看,AT&T
汇编是从左向右执行,比如如下汇编代码:
c
// AT&T 汇编风格
mov %ax, %bx
就是将寄存器ax
的内容移动到寄存器bx
。
和AT&T
汇编相反,大家比较熟悉的Intel
汇编是从右往左执行:
c
// Intel 汇编风格
mnemonic destination, source
上面的mov
使用Intel
汇编书写就是:
c
// Intel 汇编风格
mov bx, ax
2.2 寄存器
AT&T
汇编的一大特征就是寄存器前面有一个%
,比如%ax
,%rax
,%bx
,%rbx
等。
2.3 字面量
AT&T
汇编中,字面量前面都有一个$
,比如:
c
mov $100, %ax
上面汇编代码将字面量100
移动到寄存器ax
。
2.4 内存寻址方式
AT&T
的内存寻址方式通用语法为:
arduino
segment-override:signed-offset(base,index,scale)
segment-override
是段地址。
signed-offset(base, index, scale)
是段内偏移,计算方式为base + signed-offset + index * scale
。
通过段地址:段内偏移,就可以定位到具体的内存。
需要注意的是,上面内存寻址语法中的每一个部分,都可以根据实际情况省略掉。省略segment-override
段地址就是当前段,省略signed-offset
、base
、index
这些值就是0
,省略scale
就是1
。
下面给出一些寻址例子:
c
100 // 访问 当前段:100 处内存
%es:100 // 访问 es:100 处内存
(%eax) // 访问 当前段:eax 处内存
(%eax,%ebx) // 访问 当前段:(eax+ebx) 处内存
(%ecx,%ebx,2) // 访问 当前段:(ecx+ebx*2) 处内存
(,%ebx,2) // 访问 当前段:(ebx*2) 处内存
-10(%eax) // 访问 当前段:(eax-10) 处内存
%ds:-10(%ebp) // 访问 ds:(ebp-10) 处内存
2.5 操作数大小
假设有如下AT&T
汇编代码:
c
mov $100, %es:(%eax)
上面代码将字面量100
移动到内存地址es:eax
处,但是100
到底在内存中占用多少字节呢?
如果占用1
字节,那么es:eax
内存地址存储的就会是0x64
。
如果占用2
字节,那么es:eax
内存地址存储的就会是0x00
0x64
。
为了做出区分,AT&T
汇编在助记符后面添加后缀进行区分:
后缀b
代表1
个字节;
后缀w
代表2
个字节;
后缀l
代表4
个字节;
后缀q
代表8
个字节。
那么,如果上面例子100
占用4
个字节,那么正确的写法就是:
c
movl $100, %es:(%eax)
2.6 控制转移指令
AT&T
汇编中控制转移包括jump
、call
、ret
。
如果代码的转移在相同的代码段,那么就是近(Near
)转移。
如果代码转移到不同的代码段,那么就是远(Far
)转移。在远转移的情况下,助记符前面需要加上前缀l
,比如ljump
、lcall
、lret
。
转移指令中目的内存地址表示可以分为 label
、寄存器、直接内存地址、段地址-段内偏移 这4
种形式:
c
// 1. label 形式
label1:
.
.
jmp label1 // near 跳转
// 2. 寄存器形式
jmp *%eax // near 跳转,跳转到 eax 指向的地址
jmp *%ecx // near 跳转,跳转到 ecx 指向的地址
jmp *(%eax) // near 跳转,先取出 eax 指向内存处的值,然后跳转到这个地址
call *(%ebx) // near 调用,先取出 ebx 指向内存处的值,然后转移到这个地址
ljmp *(%eax) // far 跳转,先取出 eax 指向内存处的值,然后跳转到这个地址
lcall *(%ebx) // far 调用,先取出 ebx 指向内存处的值,然后转移到这个地址
// 3. 直接内存地址
jmp *100 // near 跳转,跳转到内存地址 100
call *100 // near 调用,转移到内存地址 100
ljmp *100 // far 跳转,跳转到内存地址 100
lcall *100 // far 调用,转移到内存地址 100
// 4. 段地址-段内偏移
jmp $0x10, $0x100000 // 跳转到地址 0x10:0x100000 出
从上面例子可以看到,除了第1
种和第4
种形式,其他形式地址前都需要加一个*
。
2.7 rip 寄存器相对寻址
rip 寄存器相对寻址是一种特殊的寻址方式。
假设有个一个全局变量global_var
,它位于内存0x1000
处,下面的指令:
c
movl global_var(%rip), %eax
会是什么意思呢?
如果按照上面内存寻址的计算方式,上面代码会将rip + 0x1000
内存处的值移动到寄存器eax
。
但是,实际上这是rip
寄存器相对寻址语法。
假设此时rip
寄存器存储的值是0x3000
,上面语法的等价形式是:
c
movl -0x2000(%rip), %eax
其中-0x2000
由rip - 0x1000
计算得到,所以这条语句的实际功能是将全局变量global_var
的值移动到寄存器eax
。
本文由mdnice多平台发布