大多数的资料还是介绍x86汇编,而现在的程序大多是64位了。

寄存器变动

增加了64位寄存器 R8-R15, 扩展了原来已有的寄存器RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP。原来的代码指针寄存器EIP也扩展成64位的RIP(RIP执行下一条代码)。RSP作为栈指针。

同时寄存器可以分为多个宽度来访问,比如RAX寄存器,可以使用RAX来访问64位的数据,EAX访问低位的32位数据,AX访问低位的16位数据,AH访问AX中高8位,AL访问AX中低8位。而对于64位中新增加的寄存器R8-R15,R8(访问64位)、R8D(低32位)、R8W(低16位)、R8B(低8位)。

RFLAGS寄存器保存各种操作的标志位,它是x86中EFLAGS的64位扩展。

常用标志位:

符号 位数 名称 设置
CF 0 Carry 当发生进位或者借位时
PF 2 Parity 运算得到的结果中1的个数为偶数时置为1
AF 4 Ajust 辅助进位
ZF 6 Zero 结果为0时置1
SF 7 Sign 结果为负时置1
OF 11 Overflow 溢出时置1
DF 10 Direction 字符串操作方向,置1时为增加方向

浮点数单元(FPU)包含8个寄存器FPR0-FPR7,每个寄存器是80-bit,其中可以使用MMX0-MMX7范围这些寄存器的64-bit。

指令基础

指令结构:指令 [目的操作],[源操作]

寻址指令

直接数赋值:ADD EAX, 14 ,直接将14存放到EAX中。

寄存器之间:ADD R8L, AL,将8位的AL中的值存放到R8L中。

间接存放:使用公式直接数[放大因子 * 索引值 + 基础值] ,比如MOV R8W, 1234[8*RAX+RCX]等于取地址8*RAX+RCX+1234地址的值到R8W中。当目标寄存器的宽度与内存宽度不符合时,需要指定怎么取值,比如`MOV ECX, dword ptr [RBX+RDI]

Opcode Meaning Opcode Meaning
MOV Move to/from/between memory and registers AND/OR/XOR/NOT Bitwise operations
CMOV* Various conditional moves SHR/SAR Shift right logical/arithmetic
XCHG Exchange SHL/SAL Shift left logical/arithmetic
BSWAP Byte swap ROR/ROL Rotate right/left
PUSH/POP Stack usage RCR/RCL Rotate right/left through carry bit
ADD/ADC Add/with carry BT/BTS/BTR Bit test/and set/and reset
SUB/SBC Subtract/with carry JMP Unconditional jump
MUL/IMUL Multiply/unsigned JE/JNE/JC/JNC/J* Jump if equal/not equal/carry/not carry/ many others
DIV/IDIV Divide/unsigned LOOP/LOOPE/LOOPNE Loop with ECX
INC/DEC Increment/Decrement CALL/RET Call subroutine/return
NEG Negate NOP No operation
CMP Compare CPUID CPU information

操作系统相关

理论上64位操作系统可以寻址64位地址,实际上目前的芯片使用低48位,因此寻址范围是0~0x7fff ffffffff。地址范围0~0x7fff ffff ffff 和地址范围0xffff 8000 0000 0000 ~ 0x ffff ffff ffff ffff作为虚拟地址空间。其他的空间不使用,因为完整的64位内存需要很大的page table。许多系统使用0xffff 8000 0000 0000 ~ 0x ffff ffff ffff ffff作为内核地址范围,而0~0x7fff ffff ffff作为用户空间地址范围。

调用惯例

Microsoft x64调用惯例使用fastcall:

  1. RCX、RDX、R8、R9用于从左到右的整数、指针参数。
  2. XMM0、XMM1、XMM3用于浮点参数。
  3. 剩下的参数从左到右push到栈中。
  4. 调用者在调用前需要存储RCX、RDX、XMM0等用于存放参数的寄存器。
  5. 调用者在调用后清理分配的栈空间。
  6. 浮点数返回值存放在XMM0中、整数返回值存放在RAX中。
  7. 当返回值过大时,返回值存放在栈中,由调用者分配空间,返回值地址存放在RCX中。
  8. 栈16字节对齐。
  9. 被调用者需要自己保存RAX、RBP、RDI、RSI、R12、R14、R15.

GNU C的调用惯例cdecl:

  1. 从右到左push参数到栈中。
  2. 64位系统下前6个整数、指针参数使用RDI、RSI、RDX、RCX、R8、R9传入。
  3. 函数调用者保存RBP、恢复RBP。
  4. 返回地址保存在[RSP]中,第一个变量存放在[RSP+8],依次存放。
  5. 栈指针RSP必须16字节对齐。

例子

main这个符号是默认的入口函数,所以不用在gcc里面更改入口,extern printf表示这是外部符号,在链接时重定向。RDI传入printf函数的第一个参数,RSI传入第二个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
global main

extern printf

SECTION .text

main:
push rbp
mov rsi, message
mov rdi, format
call printf
mov ax, 0
pop rbp
ret
format:
db "output: %s", 0xa, 0
message:
db "hello ss!", 0

编译链接执行:

1
nasm -felf64 libcall.S && gcc -no-pie libcall.o && ./a.out

在Ubuntu 18.04 x64的环境下。

-no-pie:关闭内核加载程序时,随机化虚拟地址的功能。PIE(Position Independent Executables)用于库函数固化的结果(an output of the hardened package build process),一个PIE二进制程序和它的依赖会被加载到虚拟内存的随机地址,使得那些覆盖return地址的攻击变得困难。

参考

内容大多来自《Introduction to x64 Assembly》这本书

PIE介绍 https://access.redhat.com/blogs/766093/posts/1975793

NSAM参考 http://cee.github.io/NASM-Tutorial/