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
- 将堆栈顶端的字复制到标志寄存器。