跳转到内容

x86 汇编/数据传输

来自 Wikibooks,开放世界中的开放书籍

一些最重要的和最常用的指令是那些移动数据的指令。没有它们,寄存器或内存甚至无法拥有任何要操作的内容。

数据传输指令

[编辑 | 编辑源代码]
mov src, dest GAS 语法
mov dest, src Intel 语法

mov 代表 移动。尽管它的名字是 移动 指令,但 mov 指令实际上是 复制 src 操作数到 dest 操作数。操作之后,两个 操作数都包含相同的内容。

操作数

[编辑 | 编辑源代码]
mov 指令的合法操作数
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 代表 交换交换 具有误导性,因为实际上没有交换任何数据。

  1. xchg 指令将 src 操作数与 dest 操作数交换。它类似于执行三个 mov 操作
  2. dest 到一个临时变量(另一个寄存器),
  3. 然后从 srcdest,最后

从临时存储到 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 代表 比较和交换交换 具有误导性,因为实际上没有交换任何数据。

  1. cmpxchg 指令有一个隐式 操作数:al/ax/eax,取决于 arg1 的大小。
  2. 该指令将 arg1al/ax/eax 进行比较。
  3. 否则,al/ax/eax 将变为 arg1

xchg 不同,它没有隐式的 lock 前缀,如果指令需要是原子的,则必须添加 lock 前缀。

操作数

[编辑 | 编辑源代码]

arg2 必须是寄存器。 arg1 可以是寄存器或内存操作数。

修改后的标志

[编辑 | 编辑源代码]
  • ZF ≔ arg1 = (al|ax|eax) [取决于 arg1 的大小]
  • CFPFAFSFOF 也会被更改。

以下示例展示了如何使用 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 指定的位置。如果方向标志已清除,则在操作后 esiedi 会递增。否则,如果方向标志已设置,则指针会递减。在这种情况下,复制将以相反的方向进行,从最高地址开始,向较低的地址移动,直到 ecx 为零。

操作数

[编辑 | 编辑源代码]

没有显式操作数,但

  • ecx 确定迭代次数,
  • esi 指定源地址,
  • edi 指定目标地址,
  • DF 用于确定方向(它可以通过 cldstd 指令更改)。

修改后的标志

[编辑 | 编辑源代码]

此指令不会修改任何标志。

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 类似,但执行取决于各种标志位。 有以下可用指令:

可用 cmovcc 组合
… = 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
Note

cmov 指令需要在平台上可用。 可以使用 cpuid 指令检查这一点。

操作数

[编辑 | 编辑源代码]

Dest 必须是寄存器。 Src 可以是寄存器或内存操作数。

cmov 指令可以用来消除 分支,因此使用 cmov 指令可以避免分支预测错误。 但是,需要谨慎使用 cmov 指令:依赖链会变长。

8086 微处理器的数据传输指令

[编辑 | 编辑源代码]

通用字节或字传输指令

mov
将指定来源的字节或字复制到指定目标。
push
将指定字复制到堆栈顶端。
pop
将堆栈顶端的字复制到指定位置。
pusha
将所有寄存器复制到堆栈。
popa
将堆栈中的字复制到所有寄存器。
xchg
交换字节或交换字。
xlat
使用内存中的表格将 al 中的字节翻译。

输入/输出

[编辑 | 编辑源代码]

这些是 I/O 端口传输指令

in
将特定端口的字节或字复制到累加器。
out
将累加器的字节或字复制到特定端口。

地址传输指令

[编辑 | 编辑源代码]

特殊地址传输指令

lea
将操作数的有效地址加载到指定寄存器。
lds
从内存中加载 DS 寄存器和其他指定寄存器。
les
从内存中加载 ES 寄存器和其他指定寄存器。

标志位

[编辑 | 编辑源代码]

标志位传输指令

lahf
将标志寄存器的低字节加载到 ah 中。
sahf
ah 寄存器存储到标志寄存器的低字节。
pushf
将标志寄存器复制到堆栈顶端。
popf
将堆栈顶端的字复制到标志寄存器。
华夏公益教科书