勇闯汇编

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

基本概念

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

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

栈的基本操作

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

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

© 2024 Null's blog

Elegant theme by Shiro · Made by Acris with ❤️

Build: 2026-07-05 17:50:09 Asia/Shanghai · 036f3ce