首页 > 编程笔记

汇编语言Irvine64链接库

本教程提供了一个能支持 64 位编程的最小链接库,其中包含了如下过程:

尽管这个库比 32 位链接库小很多,它还是包含了许多重要工具能使得程序更具互动性。随着学习的深入,大家可以用自己的代码来扩展这个链接库。Irvine64 链接库会保留 RBX、RBP、RDI、RSI、R12、R13、R14 和 R15 寄存器的值,反之,RAX、RCX、RDX、R8、R9、R10 和 R11 寄存器的值则不会保留。

调用 64 位子程序

如果想要调用自己编写的子程序,或是 Irvine64 链接库中的子程序,则程序员需要做的就是将输入参数送入寄存器,并执行 CALL 指令。比如:
mov rax,12345678h
call WriteHex64
还有一件小事也需要完成,即程序员要在自己程序的顶部用 PROTO 伪指令指定所有在本程序之外同时又将会被调用的过程:
ExitProcess PROTO         ;位于 Windows API
WriteHex64 PROTO          ;位于 Irvine64 链接库

x64 调用规范

Microsoft 在 64 位程序中使用统一模式来传递参数并调用过程,称为 Microsoft x64 调用规范。该规范由 C/C++ 编译器和 Windows 应用编程接口(API)使用。

程序员只有在调用 Windows API 的函数或用 C/C++ 编写的函数时,才会使用这个调用规范。该调用规范的一些基本特性如下所示:

1) CALL 指令将 RSP(堆栈指针)寄存器减 8,因为地址是 64 位的。

2) 前四个参数依序存入 RCX、RDX、R8 和 R9 寄存器,并传递给过程。如果只有一个参数,则将其放入 RCX。如果还有第二个参数,则将其放入 RDX,以此类推。其他参数,按照从左到右的顺序压入堆栈。

3) 调用者的责任还包括在运行时堆栈分配至少 32 字节的影子空间(shadow space),这样,被调用的过程就可以选择将寄存器参数保存在这个区域中。

4) 在调用子程序时,堆栈指针(RSP)必须进行 16 字节边界对齐(16 的倍数)。CALL 指令把 8 字节的返回值压入堆栈,因此,除了已经减去的影子空间的 32 之外,调用程序还必须从堆栈指针中减去 8。后面的示例将显示如何实现这些操作。

提示:调用 Irvine64 链接库中的子程序时,不需使用 Microsoft x64 调用规范;只在调用 Windows API 函数时使用它。

调用过程示例

现在编写一段小程序,使用 Microsoft x64 调用规范来调用子程序 AddFour。这个子程序将四个参数寄存器(RCX、RDX、R8 和 R9)的内容相加,并将和数保存到 RAX。

由于过程通常使用 RAX 返回结果,因此,当从子程序返回时,调用程序也期望返回值在这个寄存器中。这样就可以说这个子程序是一个函数,因为,它接收了四个输入并(确切地说)产生了一个输出。
;在64模式下调用子程序

ExitProcess PROTO
WriteInt64 PROTO          ;Irvine64链接库
Crlf PROTO                ;Irvine64链接库

.code
main PROC
    sub    rsp,8            ;对准堆栈指针
    sub    rsp,20h          ;为影子参数保留32个字节

    mov    rcx,1            ;依序传递参数
    mov    rdx,2
    mov    r8,3
    mov    r9,4
    call AddFour            ;在RAX中查找返回值
    call WriteInt64         ;显示数字
    call Crlf               ;输出回车换行符

    mov    ecx,0
    call ExitProcess
main ENDP

AddFour PROC
    mov rax,rcx
    add    rax,rdx
    add    rax,r8
    add    rax,r9            ;和数保存在RAX中
    ret
AddFour ENDP

END

现在来看看本例中的其他细节:第 10 行将堆栈指针对齐到 16 字节的偶数边界。为什么要这样做?在 OS 调用主程序之前,假设堆栈指针是对齐 16 字节边界的。然后,当 OS 调用主程序时,CALL 指令将 8 字节的返回地址压入堆栈。将堆栈指针再减去 8,使其减少成一个 16 的倍数。

可以在 Visual Studio 调试器中运行该程序,并查看 RSP 寄存器(堆栈指针)改变数值。通过这个方法,能够看到用图形方式在下图中展示的十六进制数值。

程序的运行时堆栈

上图只展示了每个地址的低 32 位,因为高 32 位为全零:

1) 执行第 10 行前,RSP=01AFE48。这表示在 OS 调用本程序之前,RSP 等于 01AFE50。( CALL 指令使得堆栈指针减 8。)

2) 执行第 10 行后,RSP=01AFE40,表示堆栈正好对齐到 16 字节边界。

3) 执行第 11 行后,RSP=01AFE20,表示 32 个字节的影子空间位置从 01AFE20 到 01AFE3F。

4) 在 AddFour 过程中,RSP=01AFE18,表示调用者的返回地址已经压入堆栈。

5) 从 AddFour 返回后,RSP 再一次等于 01AFE20,与调用 AddFour 之前的值相同。

与调用 ExitProcess 来结束程序相比,本程序选择的是执行 RET 指令,这将返回到启动本程序的过程。但是,这也就要求能将堆栈指针恢复到其在 main 程开始执行时的位置。下面的代码行能替代 CallProc_64 程序的第 20 和 21 行:
add rsp,28         ;恢复堆栈指针
mov ecx,0          ;过程返回码
ret                ;返回 OS

提示:要使用 Irvine64 链接库,将 Irvine64.obj 文件添加到用户的 Visual Studio 项目中。Visual Studio 中的操作步骤如下:在 Solution Explorer 窗口中右键点击项目名称,选择 Add,选择 Existing Item,再选择 Irvine64.obj 文件名。

关注公众号「站长严长生」,在手机上阅读所有教程,随时随地都能学习。本公众号由站长亲自运营,长期更新,坚持原创,持续分享创业故事+学习历程+工作记录+生活日常+编程资料。

公众号二维码
微信扫码关注公众号

优秀文章