跳到正文
Null's Blog

勇闯汇编

今天心血来潮学了汇编,以下记录学到的知识

基本概念

extern 引用外部函数:

1
2
3
extern ExitProcess
extern WriteConsoleA
extern GetStdHandle

section .data 标志初始化变量段

初始化变量格式:

1
2
3
4
5
var1 db <var_data>
msg db 'Hello, World', 0xA ;0xA是换行符

var2 equ <expression>
msg_len equ $ - msg ;表示msg变量的长度

section .text 标志可执行代码段

_start 代表程序入口点

常用指令

数据传送指令

mov 指令用于在寄存器之间或寄存器与内存之间移动数据。

1
2
mov rax, 100 ; 将立即数 100 移动到 rax 寄存器
mov rbx, rax ; 将 rax 寄存器的值复制到 rbx 寄存器

算术运算指令

  • add: 加法
  • sub: 减法
  • inc: 递增
  • dec: 递减
  • xor: 异或 (常用于将寄存器清零)
1
2
3
4
5
6
mov rax, 100
add rax, 50 ; rax = 150
sub rax, 25 ; rax = 125
inc rax ; rax = 126
dec rax ; rax = 125
xor rcx, rcx ; rcx = 0

比较与跳转指令

  • cmp: 比较两个操作数,并根据结果设置标志位。
  • jmp: 无条件跳转。
  • jne: 如果不相等则跳转 (jump if not equal)。
  • jle: 如果小于或等于则跳转 (jump if less or equal)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
; if-else 结构
mov rax, 10
mov rbx, 20
cmp rax, rbx
jne else_block ; 如果 rax != rbx, 跳转到 else_block

if_block:
mov rcx, 1
jmp end_if

else_block:
mov rcx, 99

end_if:
; ...

; 循环结构
mov rcx, 1
loop_start:
; ... 循环体 ...
inc rcx
cmp rcx, 10
jle loop_start ; 如果 rcx <= 10, 跳转到 loop_start

函数与调用约定

函数定义与调用

一个典型的函数包含函数序言、函数体和函数尾声。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
; 定义一个函数
AddTwoNumbers:
; 函数序言 (Function Prologue) - 建立栈帧
push rbp ; 保存调用者的基指针
mov rbp, rsp ; 建立我们自己的基指针

; 函数体 (Function Body)
; 参数已经由调用者放在了 rcx 和 rdx 中
mov rax, rcx ; 把第一个参数 (a) 放入 rax
add rax, rdx ; rax = rax + 第二个参数 (b)
; 现在计算结果 (a+b) 已经按约定存放在了 rax 中

; 函数尾声 (Function Epilogue) - 恢复栈帧
mov rsp, rbp ; 释放局部变量空间 (我们这里没有)
pop rbp ; 恢复调用者的基指针
ret ; 返回!(跳转到 call 指令留下的返回地址)

; 调用函数
mov rcx, 50 ; 第一个参数
mov rdx, 25 ; 第二个参数
call AddTwoNumbers
; 返回值在 rax 中

参数传递与返回值 (x64 Windows 调用约定)

在x64 Windows调用约定中,函数参数和返回值的传递有明确的规则:

  • 参数传递: 前四个整数或指针参数按从左到右的顺序依次放入 rcx, rdx, r8, r9 寄存器。更多的参数则通过栈传递。
  • 返回值: 整数或指针类型的返回值通常存储在 rax 寄存器中。

在上面的 AddTwoNumbers 例子中:

  1. mov rcx, 50 和 mov rdx, 25 将两个参数放入了正确的寄存器。
  2. 函数内部直接从 rcx 和 rdx 读取参数。
  3. add rax, rdx 的计算结果(返回值)被存放在 rax 中。
  4. 调用 call AddTwoNumbers 之后,主程序通过读取 rax 寄存器来获取函数的返回值。

栈操作与 sub rsp, 40 的重要性

栈的基本操作

push 和 pop 用于在函数调用期间保存和恢复寄存器的状态,确保函数调用不会破坏调用者的寄存器。

1
2
3
4
5
6
7
; 函数序言中保存基指针
push rbp

; ...

; 函数尾声中恢复基指针
pop rbp

sub rsp, 40 与 Shadow Space

在x64 Windows调用约定中,sub rsp, 40 这行代码至关重要。它不仅仅是为局部变量分配空间,更重要的是它遵循了调用约定中的 Shadow Space (影子空间) 规则。

  • 什么是Shadow Space?: 调用约定规定,当一个函数(调用者)准备调用另一个函数(被调用者)时,它必须在栈上为被调用者预留一块空间。这块空间被称为 “Shadow Space”。在x64 Windows上,这块空间的大小是32字节(可以容纳4个64位寄存器)。
  • 为什么需要它?: 这块空间允许被调用函数在不修改自己栈帧的情况下,方便地保存 rcx, rdx, r8, r9 这四个参数寄存器的值。
  • 为什么是40字节?: call 指令会将返回地址(8字节)压入栈中。为了保持栈的16字节对齐(这在x64下是推荐的,可以提高性能),并且为32字节的Shadow Space腾出空间,我们需要分配 32 + 8 = 40 字节。sub rsp, 40 正好满足了这个要求,它为即将到来的 call 指令做好了准备。

总结: sub rsp, 40 是在函数调用前,为满足x64 Windows调用约定而进行的关键栈操作,它分配了32字节的Shadow Space并确保了栈的对齐。

Windows API 调用详解

通过 call 指令调用 extern 声明的外部函数。以下是常用API的参数和返回值说明:

  • ExitProcess: 终止当前进程。

    • 接收值: rcx - 进程的退出码 (一个32位无符号整数)。0 通常表示成功。
    • 返回值: 无。
  • GetStdHandle: 获取标准输入、输出或错误设备的句柄。

    • 接收值: rcx - 一个指定标准设备的32位整数。常用值包括:
      • -10: 标准输入 (STD_INPUT_HANDLE)
      • -11: 标准输出 (STD_OUTPUT_HANDLE)
      • -12: 标准错误 (STD_ERROR_HANDLE)
    • 返回值: rax - 返回一个句柄。如果失败,返回 INVALID_HANDLE_VALUE (-1)。
  • WriteConsoleA: 将一个ASCII字符串写入控制台。

    • 接收值:
      • rcx: 通过 GetStdHandle 获取的控制台输出句柄。
      • rdx: 指向要写入的字符串缓冲区的指针。
      • r8: 要写入的字符数。
      • r9: 指向一个变量的指针,该变量用于接收实际写入的字符数(可以为0,表示不关心)。
      • [rsp+32]: (栈上传递的第五个参数) 必须为0。
    • 返回值: rax - 如果函数成功,返回非零值。如果失败,返回零。
1
2
3
4
5
6
7
8
9
10
11
12
; 示例:打印 "Hello, World!"
mov rcx, -11 ; 获取标准输出句柄
call GetStdHandle

mov rcx, rax ; 第一个参数:句柄
mov rdx, msg ; 第二个参数:字符串地址
mov r8, msg_len ; 第三个参数:字符串长度
mov r9, 0 ; 第四个参数:不关心写入的字符数
sub rsp, 32 ; 为第五个参数在栈上分配空间
mov QWORD [rsp+32], 0 ; 第五个参数:必须为0
call WriteConsoleA
add rsp, 32 ; 清理为第五个参数分配的栈空间

一个脚本

我还写了一个Python脚本用来帮我快速编译asm文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import sys
import os
import subprocess

# ANSI escape codes for colors
class Color:
RESET = '\033[0m'
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
MAGENTA = '\033[35m'
CYAN = '\033[36m'

def run_command(command):
"""Runs a command and checks for errors, handling different encodings."""
print(f"{Color.CYAN}执行命令: {Color.YELLOW}{' '.join(command)}{Color.RESET}")
# Run command without text decoding to get raw bytes
result = subprocess.run(command, capture_output=True, shell=True)

# Try to decode output with gbk, fallback to utf-8
try:
stdout = result.stdout.decode('gbk')
stderr = result.stderr.decode('gbk')
except UnicodeDecodeError:
stdout = result.stdout.decode('utf-8', errors='ignore')
stderr = result.stderr.decode('utf-8', errors='ignore')

if result.returncode != 0:
print(f"{Color.RED}错误: 执行命令失败: {' '.join(command)}{Color.RESET}")
if stdout.strip():
print(f"{Color.RED}标准输出:\n{stdout.strip()}{Color.RESET}")
if stderr.strip():
print(f"{Color.RED}标准错误:\n{stderr.strip()}{Color.RESET}")
sys.exit(1)

print(f"{Color.GREEN}成功。{Color.RESET}")
if stdout.strip():
print(f"{Color.YELLOW}标准输出:\n{stdout.strip()}{Color.RESET}")

def main():
"""Main function to assemble and link the asm file."""
if len(sys.argv) < 2:
print(f"{Color.RED}用法: python asmc.py <文件名.asm> [库...]{Color.RESET}")
sys.exit(1)

asm_file = sys.argv[1]
if not asm_file.lower().endswith('.asm'):
print(f"{Color.RED}错误: 第一个参数必须是 .asm 文件, 但收到的是 {asm_file}{Color.RESET}")
sys.exit(1)

base_name = os.path.splitext(asm_file)[0]
obj_file = base_name + '.obj'
exe_file = base_name + '.exe'

libs = sys.argv[2:]

# 1. nasm -f win64 xxx.asm -o xxx.obj
print(f"\n{Color.MAGENTA}--- 步骤 1: 汇编文件 ---{Color.RESET}")
nasm_command = ['nasm', '-f', 'win64', asm_file, '-o', obj_file]
run_command(nasm_command)

# 2. link xxx.obj /subsystem:console /entry:_start /out:xxx.exe xxx.lib ...
print(f"\n{Color.MAGENTA}--- 步骤 2: 链接目标文件 ---{Color.RESET}")
link_command = ['link', obj_file, '/subsystem:console', '/entry:_start', f'/out:{exe_file}'] + libs
run_command(link_command)

# 3. del xxx.obj
print(f"\n{Color.MAGENTA}--- 步骤 3: 清理 ---{Color.RESET}")
print(f"{Color.CYAN}正在删除: {Color.YELLOW}{obj_file}{Color.RESET}")
try:
os.remove(obj_file)
print(f"{Color.GREEN}成功。{Color.RESET}")
except OSError as e:
print(f"{Color.RED}错误: 删除文件 {obj_file} 失败: {e}{Color.RESET}")
sys.exit(1)

print(f"\n{Color.GREEN}所有步骤已成功完成!已生成可执行文件: {Color.YELLOW}{exe_file}{Color.RESET}")

if __name__ == "__main__":
main()

用法:
python asmc.py <your_file.asm> [library1.lib library2.lib …]
例如:
python asmc.py hello.asm kernel32.lib

加载不畅或无法显示时,可尝试开启代理(梯子)后刷新。