目录
今天心血来潮学了汇编,以下记录学到的知识 基本概念 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 例子中:
mov rcx, 50 和 mov rdx, 25 将两个参数放入了正确的寄存器。
函数内部直接从 rcx 和 rdx 读取参数。
add rax, rdx 的计算结果(返回值)被存放在 rax 中。
调用 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的参数和返回值说明:
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 sysimport osimport subprocessclass 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} " ) result = subprocess.run(command, capture_output=True , shell=True ) 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 :] print (f"\n{Color.MAGENTA} --- 步骤 1: 汇编文件 ---{Color.RESET} " ) nasm_command = ['nasm' , '-f' , 'win64' , asm_file, '-o' , obj_file] run_command(nasm_command) 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) 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
←
Arduino<-让我看看!
浣溪沙·节假日
→