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)。相反,一个专用的 push
和 pop
指令(尝试)从堆栈中检索或存储一个值。使用它们是“慢的”,因为,由于只有一个标志寄存器,所有挂起的(潜在的)写入或读取必须先执行,然后才能获得或覆盖实际值。此外,可以读取或覆盖的内容取决于权限。
pushf
此指令递减堆栈指针,然后将堆栈指针指向的位置加载为标志寄存器内容的屏蔽副本。RF 和 VM 标志始终在副本中被清除。在某些情况下,可能会出现 GPF。
popf
此指令尽可能尝试使用堆栈指针指向的内存位置的内容加载标志寄存器,然后递增堆栈指针的内容。一些标志可能会保留其原始值,即使请求这样做也是如此。如果缺乏更改某些或任何值的权限,则会发生 GPF。
在 OS 开发(如 线程)之外,这些指令的标准使用情况是检查 cpuid
指令是否可用。如果可以更改 EFLAGS 寄存器中的 ID 标志,则支持 cpuid
指令。
检查 cpuid 的函数示例 |
---|
这里,我们假设我们拥有检索和覆盖标志寄存器的适当权限。在此示例中,使用此函数的编程语言要求 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
指令之后是递减还是递增 esi
和 edi
寄存器。
std
设置方向标志。寄存器将递减,向后读取。
cld
清除方向标志。寄存器将递增,向前读取。
CF 标志通常在算术指令后被修改,但它也可以手动设置或清除。
stc
设置进位标志。
clc
清除进位标志。
cmc
对进位标志求反(反转)。
sahf
将 AH 寄存器的内容存储到标志寄存器的低字节中。
lahf
将标志寄存器的低字节的内容加载到 AH 寄存器中。
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]
|
这些指令是在奔腾 II 中添加的。
sysenter
此指令导致处理器进入受保护系统模式(监管模式或“内核模式”)。
sysexit
此指令导致处理器退出受保护系统模式,并进入用户模式。
RDTSC
RDTSC 是在奔腾处理器中引入的,该指令读取自复位以来的时钟周期数,并将结果返回到 EDX:EAX 中。这可以用作获得低开销、高分辨率 CPU 定时的途径。虽然在现代 CPU 微体系结构(多核、超线程)和多 CPU 机器上,您无法保证内核和 CPU 之间的时间戳计数器同步。此外,由于节能或动态超频,CPU 频率可能会有变化。因此,该指令的可靠性可能不如首次引入时,在用于性能测量时应谨慎使用。
可以使用结果的低 32 位,但需要注意的是,在 600 MHz 处理器上,寄存器每 7.16 秒就会溢出
-
()
而使用完整的 64 位,则溢出间隔为 974.9 年
-
()
以下程序(使用 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
- ↑ 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.
- ↑ "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.