汇编语言Irvine64链接库


本教程提供了一个能支持 64 位编程的最小链接库,其中包含了如下过程:
  • Crlf:向控制台写一个行结束的序列。
  • Random64:在0〜2⁶⁴-1 内,生成一个 64 位的伪随机整数。随机数值用 RAX 寄存器返回。
  • Randomize:用一个值作为随机数生成器的种子。
  • Readlnt64:从键盘读取一个 64 位有符号整数,用回车符结束。数值用 RAX 寄存器返回。
  • ReadString:从键盘读取一个字符串,用回车符结束。过程用 RDX 传递输入缓冲器偏移量;用 RCX 传递用户可输入的最大字符数加 1(用于 unll 结束符字节)。返回值(用 RAX)为用户实际输入的字符数。
  • Str_compare:比较两个字符串。过程将源串指针传递给 RSI,将目的串指针传递给 RDIO 用与 CMP(比较)指令一样的方式设置零标志位和进位标志位。
  • Str_copy:将一个源串复制到目标指针指定的位置。源串偏移量传递给 RSI,目标偏移量传递给 RDI。
  • Strjength:用 RAX 寄存器返回一个空字节结束的字符串的长度。过程用 RCX 传递字符串的偏移量。
  • Writelnt64:将 RAX 寄存器中的内容显示为 64 位有符号十进制数,并加上前置加号或减号。过程没有返回值。
  • WriteHex64:将 RAX 寄存器中的内容显示为 64 位十六进制数。过程没有返回值。
  • WriteHexB:将 RAX 寄存器中的内容显示为 1 字节、2 字节、4 字节或 8 字节的十六进制数。将显示的大小(1、2、4 或 8) 传递给 RBX 寄存器。过程没有返回值。
  • WriteString:显示一个空字节结束的 ASCII 字符串。将字符串的 64 位偏移量传递给 RDX。过程没有返回值。

尽管这个库比 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 文件名。