跳转到内容

x86 汇编/其他指令

来自维基教科书,自由的教科书

堆栈指令

[编辑 | 编辑源代码]

有一些专门的指令用于与 堆栈 交互。

push arg

此指令递减堆栈指针,并将作为参数指定的 data 存储到堆栈指针指向的位置。

pop arg

此指令将存储在堆栈指针指向的位置的数据加载到指定的参数中,然后递增堆栈指针。例如

mov eax, 5
mov ebx, 6
push eax 
堆栈现在是: [5]
push ebx 
堆栈现在是: [6] [5]
pop eax 
最上面的项(即 6)现在存储在 eax 中。堆栈现在是: [5]
pop ebx 
ebx 现在等于 5。堆栈现在为空。

pusha

此指令将所有通用寄存器按以下顺序推入堆栈:AX、CX、DX、BX、SP、BP、SI、DI。推入的 SP 值是在指令执行之前的值。它对于在可能更改这些寄存器的操作之前保存状态很有用。

popa

此指令按与 PUSHA 相反的顺序从堆栈中弹出所有通用寄存器。也就是说,DI、SI、BP、SP、BX、DX、CX、AX。用于在调用 PUSHA 后恢复状态。

pushad

此指令的工作原理类似于 pusha,但将 32 位通用寄存器推入堆栈,而不是它们的 16 位对应物。

popad

此指令的工作原理类似于 popa,但从堆栈中弹出 32 位通用寄存器,而不是它们的 16 位对应物。

由于大多数指令以某种方式改变标志,因此标志寄存器被认为是非常易变的。因此,在微处理器架构设计中,它不能直接查询或更改(除了少数几个单独的标志,例如 DF)。相反,一个专用的 pushpop 指令(尝试)从堆栈中检索或存储一个值。使用它们是“慢的”,因为,由于只有一个标志寄存器,所有挂起的(潜在的)写入或读取必须先执行,然后才能获得或覆盖实际值。此外,可以读取或覆盖的内容取决于权限。

pushf

此指令递减堆栈指针,然后将堆栈指针指向的位置加载为标志寄存器内容的屏蔽副本。RFVM 标志始终在副本中被清除。在某些情况下,可能会出现 GPF

popf

此指令尽可能尝试使用堆栈指针指向的内存位置的内容加载标志寄存器,然后递增堆栈指针的内容。一些标志可能会保留其原始值,即使请求这样做也是如此。如果缺乏更改某些或任何值的权限,则会发生 GPF

OS 开发(如 线程)之外,这些指令的标准使用情况是检查 cpuid 指令是否可用。如果可以更改 EFLAGS 寄存器中的 ID 标志,则支持 cpuid 指令。

检查 cpuid 的函数示例

这里,我们假设我们拥有检索和覆盖标志寄存器的适当权限。在此示例中,使用此函数的编程语言要求 Boolean 值恰好为 0 或 1

pushfq              ; put RFLAGS on top of stack
mov rax, [rsp]      ; preserve copy for comparison
xor [rsp], $200000  ; flip bit in copy
popfq               ; _attempt_ to overwrite RFLAGS

pushfq              ; obtain possibly altered RFLAGS
pop rcx             ; rcx ≔ rsp↑; inc(rsp, 8)
xor rax, rcx        ; cancel out any _unchanged_ bits
shr eax, 20         ; move ID flag into bit position 0


标志指令

[编辑 | 编辑源代码]

虽然 标志 寄存器用于报告已执行指令的结果(溢出、进位等),但它还包含影响处理器操作的标志。这些标志使用特殊指令设置和清除。

中断标志

[编辑 | 编辑源代码]

IF 标志告诉处理器是否应该接受硬件中断。在正常执行期间应保持设置。事实上,在保护模式下,用户级程序不能执行这些指令中的任何一个。

sti

设置中断标志。如果设置,处理器可以接受来自外围硬件的中断。

cli

清除中断标志。硬件中断不能中断执行。程序仍然可以生成中断,称为软件中断,并改变执行流程。不可屏蔽中断 (NMI) 不能使用此指令阻止。

方向标志

[编辑 | 编辑源代码]

DF 标志告诉处理器在使用 字符串 指令时以何种方式读取数据。也就是说,在 movs 指令之后是递减还是递增 esiedi 寄存器。

std

设置方向标志。寄存器将递减,向后读取。

cld

清除方向标志。寄存器将递增,向前读取。

进位标志

[编辑 | 编辑源代码]

CF 标志通常在算术指令后被修改,但它也可以手动设置或清除。

stc

设置进位标志。

clc

清除进位标志。

cmc

对进位标志求反(反转)。

sahf

将 AH 寄存器的内容存储到标志寄存器的低字节中。

lahf

将标志寄存器的低字节的内容加载到 AH 寄存器中。

I/O 指令

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


IN 指令几乎总是与 AX 和 DX(或 EAX 和 EDX)操作数相关联。DX(src)通常包含要读取的端口地址,而 AX(dest)接收来自端口的数据。在受保护模式操作系统中,IN 指令通常被锁定,普通用户无法在他们的程序中使用它。


out src, dest GAS 语法
out dest, src Intel 语法


OUT 指令与 IN 指令非常相似。OUT 将数据从给定的寄存器(src)输出到给定的输出端口(dest)。在受保护模式下,OUT 指令通常被锁定,因此普通用户无法使用它。

无操作指令

[编辑 | 编辑源代码]

x86 指令集有一个 NOP(无操作)指令助记符

nop


它有一个单字节操作码,0x90。此指令除了递增指令指针(EIP)之外没有副作用。尽管它的名字是“什么也不做”的指令,但它对于执行速度优化很有用。它经常被优化编译器/汇编器使用,并且可以在反汇编代码中看到散布在周围,但几乎从未在手动编写的汇编代码中使用

为了说明,一些nop 指令的应用是

  • 将以下指令与内存块的开头对齐;
  • 对齐一系列跳转目标;
  • 在对可执行文件进行二进制修补时填充空间,例如移除分支,而不是保留死代码(从未执行的代码)。


多字节无操作指令

来自 AMD[1] 和 Intel[2] 的 x86 扩展(包括 x86-64)包括多字节无操作指令。实际上,任何没有副作用的有效指令都可以用作无操作指令。下面列出了一些上述手册中推荐的版本。

Size (bytes)  Opcode (hexadecimal)       Encoding
---------------------------------------------------------------------------------
1             90                         NOP
2             66 90                      66 NOP
3             0F 1F 00                   NOP DWORD ptr [EAX]
4             0F 1F 40 00                NOP DWORD ptr [EAX + 00H]
5             0F 1F 44 00 00             NOP DWORD ptr [EAX + EAX*1 + 00H]
6             66 0F 1F 44 00 00          NOP DWORD ptr [AX + AX*1 + 00H]
7             0F 1F 80 00 00 00 00       NOP DWORD ptr [EAX + 00000000H]
8             0F 1F 84 00 00 00 00 00    NOP DWORD ptr [AX + AX*1 + 00000000H]

因为这种填充代码是可执行的,所以它应该占用尽可能少的执行资源,不应该降低解码密度,并且不应该修改除递增指令指针 (rIP) 之外的任何处理器状态。[1]


系统指令

[编辑 | 编辑源代码]

这些指令是在奔腾 II 中添加的。

sysenter

此指令导致处理器进入受保护系统模式(监管模式或“内核模式”)。

sysexit

此指令导致处理器退出受保护系统模式,并进入用户模式。

杂项指令

[编辑 | 编辑源代码]

读取时间戳计数器

[编辑 | 编辑源代码]

RDTSC

RDTSC 是在奔腾处理器中引入的,该指令读取自复位以来的时钟周期数,并将结果返回到 EDX:EAX 中。这可以用作获得低开销、高分辨率 CPU 定时的途径。虽然在现代 CPU 微体系结构(多核、超线程)和多 CPU 机器上,您无法保证内核和 CPU 之间的时间戳计数器同步。此外,由于节能或动态超频,CPU 频率可能会有变化。因此,该指令的可靠性可能不如首次引入时,在用于性能测量时应谨慎使用。

可以使用结果的低 32 位,但需要注意的是,在 600 MHz 处理器上,寄存器每 7.16 秒就会溢出

 

 

 

 

( 0 )

而使用完整的 64 位,则溢出间隔为 974.9 年

 

 

 

 

( 1 )

以下程序(使用 NASM 语法)是使用 RDTSC 测量一小块代码执行所需周期的示例

global main 

extern printf

section .data
	align 4
	a:	dd 10.0
	b:	dd 5.0
	c:	dd 2.0
	fmtStr:	db "edx:eax = %llu edx = %d eax = %d", 0x0A, 0

section .bss
	align 4
	cycleLow:	resd 1
	cycleHigh:	resd 1
	result:		resd 1

section .text
	main:			; Using main since we are using gcc to link

;
;	op	dst,  src
;
	xor	eax, eax
	cpuid
	rdtsc
	mov	[cycleLow], eax
	mov	[cycleHigh], edx 

				;
				; Do some work before measurements 
				;
	fld	dword [a]
	fld	dword [c]
	fmulp	st1
	fmulp	st1
	fld	dword [b]
	fld	dword [b]
	fmulp	st1
	faddp	st1
	fsqrt
	fstp	dword [result]
				;
				; Done work
				;

	cpuid
	rdtsc
				;
				; break points so we can examine the values
				; before we alter the data in edx:eax and
				; before we print out the results.
				;
break1:
	sub	eax, [cycleLow]
	sbb	edx, [cycleHigh]
break2:
	push	eax
	push	edx
	push 	edx
	push	eax
	push	dword fmtStr
	call	printf
	add	esp, 20		; Pop stack 5 times 4 bytes


				;
				; Call _exit(2) syscall
				;	noreturn void _exit(int status)
				;
	mov	ebx, 0		; Arg one: the 8-bit status
	mov	eax, 1		; Syscall number:
	int 	0x80

为了汇编、链接和运行该程序,我们需要执行以下操作

$ nasm -felf -g rdtsc.asm -l rdtsc.lst
$ gcc -m32 -o rdtsc rdtsc.o
$ ./rdtsc

参考文献

[编辑 | 编辑源代码]
  1. a b "5.8 "Code Padding with Operand-Size Override and Multibyte NOP"". AMD Software Optimization Guide for AMD Family 15h Processors, document #47414. p. 94. http://support.amd.com/TechDocs/47414_15h_sw_opt_guide.pdf. 
  2. "NOP". Intel 64 and IA-32 Architectures Software Developer's Manual. 2B: Instruction Set Reference. http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-2b-manual.pdf. 
华夏公益教科书