x86 汇编/数据传输
一些最重要的和最常用的指令是那些移动数据的指令。没有它们,寄存器或内存甚至无法拥有任何要操作的内容。
| mov src, dest | GAS 语法 |
| mov dest, src | Intel 语法 |
mov 代表 移动。尽管它的名字是 移动 指令,但 mov 指令实际上是 复制 src 操作数到 dest 操作数。操作之后,两个 操作数都包含相同的内容。
src 操作数 |
dest 操作数 | ||
|---|---|---|---|
| 立即值 | 寄存器 | 内存 | |
| 是 (到 更大 的寄存器) |
是 (相同大小) |
是 (寄存器决定检索的内存大小) |
寄存器 |
| 是 (最多 32 位值) |
是 | 否 | 内存 |
- 此指令不会修改任何标志
.data
value:
.long 2
.text
.globl _start
_start:
movl $6, %eax # eax ≔ 6
# └───────┐
movw %eax, value # value ≔ eax
# └───────────┐
movl $0, %ebx # ebx ≔ 0 │ │
# ┌──┘ │
movb %al, %bl # bl ≔ al │
# %ebx is now 6 │
# ┌─────┘
movl value, %ebx # ebx ≔ value
movl $value, %esi # esi ≔ @value
# %esi is now the address of value
xorl %ebx, %ebx # ebx ≔ ebx ⊻ ebx
# %ebx is now 0
movw value(, %ebx, 1), %bx # bx ≔ value[ebx*1]
# %ebx is now 6
# Linux sys_exit
movl $1, %eax # eax ≔ 1
xorl %ebx, %ebx # ebx ≔ 0
int $0x80
| xchg src, dest | GAS 语法 |
| xchg dest, src | Intel 语法 |
xchg 代表 交换。交换 具有误导性,因为实际上没有交换任何数据。
xchg指令将src操作数与dest操作数交换。它类似于执行三个mov操作- 从
dest到一个临时变量(另一个寄存器), - 然后从
src到dest,最后
从临时存储到 src,
除了不需要为临时存储预留任何寄存器。
操作数
这种三个连续mov 指令的交换模式可以被某些架构中存在的 DFU 检测到,这将触发特殊处理。但是,xchg 的操作码更短。任何寄存器或内存操作数的组合,除了最多一个操作数可以是内存操作数。你不能交换两个内存块。
修改后的标志示例
.data
value:
.long 2
.text
.global _start
_start:
movl $54, %ebx
xorl %eax, %eax
xchgl value, %ebx
# %ebx is now 2
# value is now 54
xchgw %ax, value
# Value is now 0
# %eax is now 54
xchgb %al, %bl
# %ebx is now 54
# %eax is now 2
xchgw value(%eax), %ax
# value is now 0x00020000 = 131072
# %eax is now 0
# Linux sys_exit
mov $1, %eax
xorl %ebx, %ebx
int $0x80
无。
应用如果其中一个操作数是内存地址,那么该操作具有隐式 lock 前缀,也就是说,交换操作是原子的。这可能会导致较大的性能损失。
- 然而,在某些平台上,交换两个(非部分)寄存器将触发寄存器重命名。寄存器重命名器是一个单元,它仅仅重命名寄存器,因此实际上不需要移动任何数据。这非常快(被称为“零延迟”)。重命名寄存器可能有用,因为
- 某些指令要求某些操作数位于特定寄存器中,但以后还需要数据,
或者如果其中一个操作数是累加器寄存器,则编码一些操作码会更短。
xchg 指令用于更改 16 位值的字节顺序(LE ↔ BE),因为 bswap 指令仅适用于 32 位和 64 位值。你可以通过寻址部分寄存器来做到这一点,例如 xchg ah, al。
还值得注意的是,常见的 nop(无操作)指令,0x90,是 xchgl %eax, %eax 的操作码。
基于比较的数据交换| [编辑 | 编辑源代码] | GAS 语法 |
| cmpxchg arg2, arg1 | Intel 语法 |
cmpxchg arg1, arg2
cmpxchg 代表 比较和交换。交换 具有误导性,因为实际上没有交换任何数据。
cmpxchg指令有一个隐式 操作数:al/ax/eax,取决于arg1的大小。- 该指令将
arg1与al/ax/eax进行比较。 - 否则,
al/ax/eax将变为arg1。
与 xchg 不同,它没有隐式的 lock 前缀,如果指令需要是原子的,则必须添加 lock 前缀。
arg2 必须是寄存器。 arg1 可以是寄存器或内存操作数。
ZF≔arg1= (al|ax|eax) [取决于arg1的大小]CF、PF、AF、SF、OF也会被更改。
以下示例展示了如何使用 cmpxchg 指令来创建一个自旋锁,用于保护 result 变量。最后一个获取自旋锁的线程将能够设置 result 的最终值。
| 自旋锁示例 |
|---|
global main
extern printf
extern pthread_create
extern pthread_exit
extern pthread_join
section .data
align 4
sLock: dd 0 ; The lock, values are:
; 0 unlocked
; 1 locked
tID1: dd 0
tID2: dd 0
fmtStr1: db "In thread %d with ID: %02x", 0x0A, 0
fmtStr2: db "Result %d", 0x0A, 0
section .bss
align 4
result: resd 1
section .text
main: ; Using main since we are using gcc to link
;
; Call pthread_create(pthread_t *thread, const pthread_attr_t *attr,
; void *(*start_routine) (void *), void *arg);
;
push dword 0 ; Arg Four: argument pointer
push thread1 ; Arg Three: Address of routine
push dword 0 ; Arg Two: Attributes
push tID1 ; Arg One: pointer to the thread ID
call pthread_create
push dword 0 ; Arg Four: argument pointer
push thread2 ; Arg Three: Address of routine
push dword 0 ; Arg Two: Attributes
push tID2 ; Arg One: pointer to the thread ID
call pthread_create
;
; Call int pthread_join(pthread_t thread, void **retval) ;
;
push dword 0 ; Arg Two: retval
push dword [tID1] ; Arg One: Thread ID to wait on
call pthread_join
push dword 0 ; Arg Two: retval
push dword [tID2] ; Arg One: Thread ID to wait on
call pthread_join
push dword [result]
push dword fmtStr2
call printf
add esp, 8 ; Pop stack 2 times 4 bytes
call exit
thread1:
pause
push dword [tID1]
push dword 1
push dword fmtStr1
call printf
add esp, 12 ; Pop stack 3 times 4 bytes
call spinLock
mov [result], dword 1
call spinUnlock
push dword 0 ; Arg one: retval
call pthread_exit
thread2:
pause
push dword [tID2]
push dword 2
push dword fmtStr1
call printf
add esp, 12 ; Pop stack 3 times 4 bytes
call spinLock
mov [result], dword 2
call spinUnlock
push dword 0 ; Arg one: retval
call pthread_exit
spinLock:
push ebp
mov ebp, esp
mov edx, 1 ; Value to set sLock to
spin: mov eax, [sLock] ; Check sLock
test eax, eax ; If it was zero, maybe we have the lock
jnz spin ; If not try again
;
; Attempt atomic compare and exchange:
; if (sLock == eax):
; sLock <- edx
; zero flag <- 1
; else:
; eax <- edx
; zero flag <- 0
;
; If sLock is still zero then it will have the same value as eax and
; sLock will be set to edx which is one and therefore we aquire the
; lock. If the lock was acquired between the first test and the
; cmpxchg then eax will not be zero and we will spin again.
;
lock cmpxchg [sLock], edx
test eax, eax
jnz spin
pop ebp
ret
spinUnlock:
push ebp
mov ebp, esp
mov eax, 0
xchg eax, [sLock]
pop ebp
ret
exit:
;
; Call exit(3) syscall
; void exit(int status)
;
mov ebx, 0 ; Arg one: the status
mov eax, 1 ; Syscall number:
int 0x80
为了编译、链接和运行程序,我们需要执行以下操作: $ nasm -felf32 -g cmpxchgSpinLock.asm
$ gcc -o cmpxchgSpinLock cmpxchgSpinLock.o -lpthread
$ ./cmpxchgSpinLock
|
| movz src, dest | GAS 语法 |
| movzx dest, src | Intel 语法 |
movz 代表 带有零扩展的移动。与常规的 mov 一样,movz 指令将数据从 src 操作数复制到 dest 操作数,但 dest 中未由 src 提供的剩余位将用零填充。此指令适用于将较小的 无符号 值复制到更大的寄存器。
Dest 必须是寄存器,而 src 可以是另一个寄存器或内存操作数。为了使此操作有意义,dest 必须 大于 src。
没有。
.data
byteval:
.byte 204
.text
.global _start
_start:
movzbw byteval, %ax
# %eax is now 204
movzwl %ax, %ebx
# %ebx is now 204
movzbl byteval, %esi
# %esi is now 204
# Linux sys_exit
mov $1, %eax
xorl %ebx, %ebx
int $0x80
| movs src, dest | GAS 语法 |
| movsx dest, src | Intel 语法 |
movsx 代表 带有符号扩展的移动。 movsx 指令将 src 操作数复制到 dest 操作数,并将 src 未提供的剩余位用 src 的符号位(MSB)填充。
此指令适用于将 带符号 的较小值复制到更大的寄存器。
movsx 接受与 movzx 相同的操作数。
movsx 也不会修改任何标志。
.data
byteval:
.byte -24 # = 0xe8
.text
.global _start
_start:
movsbw byteval, %ax
# %ax is now -24 = 0xffe8
movswl %ax, %ebx
# %ebx is now -24 = 0xffffffe8
movsbl byteval, %esi
# %esi is now -24 = 0xffffffe8
# Linux sys_exit
mov $1, %eax
xorl %ebx, %ebx
int $0x80
movsb
移动字节。
movsb 指令将 esi 指定的内存位置中的一个字节复制到 edi 指定的位置。如果方向标志已清除,则在操作后 esi 和 edi 会递增。否则,如果方向标志已设置,则指针会递减。在这种情况下,复制将以相反的方向进行,从最高地址开始,向较低的地址移动,直到 ecx 为零。
没有显式操作数,但
ecx确定迭代次数,esi指定源地址,edi指定目标地址,- DF 用于确定方向(它可以通过
cld和std指令更改)。
此指令不会修改任何标志。
section .text
; copy mystr into mystr2
mov esi, mystr ; loads address of mystr into esi
mov edi, mystr2 ; loads address of mystr2 into edi
cld ; clear direction flag (forward)
mov ecx,6
rep movsb ; copy six times
section .bss
mystr2: resb 6
section .data
mystr db "Hello", 0x0
movsw
移动字
movsw 指令将 esi 指定的位置中的一个字(两个字节)复制到 edi 指定的位置。它基本上与 movsb 做同样的事情,只是用字而不是字节。
操作数
修改后的标志
- 此指令不会修改任何标志
示例
section .code
; copy mystr into mystr2
mov esi, mystr
mov edi, mystr2
cld
mov ecx,4
rep movsw
; mystr2 is now AaBbCca\0
section .bss
mystr2: resb 8
section .data
mystr db "AaBbCca", 0x0
| lea src, dest | GAS 语法 |
| lea dest, src | Intel 语法 |
lea 代表 加载有效地址。 lea 指令计算 src 操作数的地址,并将其加载到 dest 操作数中。
src
- 立即数
- 寄存器
- 内存
dest
- 寄存器
- 此指令不会修改任何标志
计算有效地址的方式与 mov 指令相同,但它不是将该地址的 *内容* 加载到 dest 操作数中,而是加载地址本身。
lea 不仅可以用于计算地址,还可以用于通用的无符号整数运算(需要注意的是,标志位不会被修改,这可能是一种优势)。 这非常强大,因为 src 操作数最多可以包含 4 个参数:基址寄存器、索引寄存器、标量乘数和位移量,例如 [eax + edx*4 -4](Intel 语法)或 -4(%eax, %edx, 4)(GAS 语法)。 标量乘数被限制为常数值 1、2、4 或 8,分别对应字节、字、双字或四字偏移量。 这本身允许将通用寄存器乘以常数值 2、3、4、5、8 和 9,如下所示(使用 NASM 语法)
lea ebx, [ebx*2] ; Multiply ebx by 2
lea ebx, [ebx*8+ebx] ; Multiply ebx by 9, which totals ebx*18
| cmovcc src, dest | GAS 语法 |
| cmovcc dest, src | Intel 语法 |
cmov 代表 *条件移动*。 它与 mov 类似,但执行取决于各种标志位。 有以下可用指令:
… = 1 |
… = 0 | |
|---|---|---|
| ZF | cmovz, cmove |
cmovnz, cmovne |
| OF | cmovo
|
cmovno
|
| SF | cmovs
|
cmovns
|
| CF | cmovc, cmovb, cmovnae |
cmovnc, cmovnb, cmovae |
| CF ∨ ZF | cmovbe
|
N/A |
| PF | cmovp, cmovpe |
cmovnp, cmovpo |
| SF = OF | cmovge, cmovnl |
cmovnge, cmovl |
| ZF ∨ SF ≠ OF | cmovng, cmovle |
N/A |
| CF ∨ ZF | cmova
|
N/A |
| ¬CF | SF = OF | |
| ¬ZF | cmovnbe, cmova |
cmovg, cmovnle |
|
|
Dest 必须是寄存器。 Src 可以是寄存器或内存操作数。
cmov 指令可以用来消除 分支,因此使用 cmov 指令可以避免分支预测错误。 但是,需要谨慎使用 cmov 指令:依赖链会变长。
通用字节或字传输指令
mov- 将指定来源的字节或字复制到指定目标。
push- 将指定字复制到堆栈顶端。
pop- 将堆栈顶端的字复制到指定位置。
pusha- 将所有寄存器复制到堆栈。
popa- 将堆栈中的字复制到所有寄存器。
xchg- 交换字节或交换字。
xlat- 使用内存中的表格将
al中的字节翻译。
这些是 I/O 端口传输指令
in- 将特定端口的字节或字复制到累加器。
out- 将累加器的字节或字复制到特定端口。
特殊地址传输指令
lea- 将操作数的有效地址加载到指定寄存器。
lds- 从内存中加载 DS 寄存器和其他指定寄存器。
les- 从内存中加载 ES 寄存器和其他指定寄存器。
标志位传输指令
lahf- 将标志寄存器的低字节加载到
ah中。 sahf- 将
ah寄存器存储到标志寄存器的低字节。 pushf- 将标志寄存器复制到堆栈顶端。
popf- 将堆栈顶端的字复制到标志寄存器。