64位驱动中加入汇编代码

时间:2024-05-20 08:51:42

本文介绍如何在驱动程序的C文件中调用ASM文件中导出的汇编函数
引言
    Windows驱动程序使用DDK或者IFSDDK(以下简称DDK)中的Build.exe程序对源文件进行编译和链接,操作时只需启动相应的DDK命令行程序,进入待编译的源文件目录,执行Build命令即可得到.sys文件。
    Build命令实际上是调用了一个nmake程序,nmake程序随后调用cl.exe和link.exe,并指定所有的编译和链接选项,与应用程序不同,程序员在使用build命令时无法对编译链接选项进行修改,从而无法在build命令中干预编译或者链接过程。
    64位平台不再支持内嵌式的汇编代码,为了使驱动程序中仍然可以使用汇编语言,通常的做法是将汇编代码写成独立的函数放入单独的汇编文件中,并对汇编文件进行编译形成obj文件,而后在链接期对所有目标文件(包括驱动C代码的目标文件)进行链接。但是DDK内置的Build程序并不直接支持对汇编文件的编译和链接(也可能我还没有发现这样的方法)。因此需要通过其他手段将汇编代码目标文件和C代码目标文件进行链接。本文讲述的方法是通过修改SOURCE文件和强制指定外部函数调用约定的方式来达到在Windows驱动程序中调用独立汇编函数的目的。
需要注意的问题
1.32位平台和64位平台中调用约定的不同
    32位平台下,驱动程序默认采用__stdcall调用约定,该约定编译生成的函数名均带有后缀@xxx,而汇编代码采用__cdecl调用约定,用汇编器编译生成的函数名并不带后缀,从而导致C代码无法引用汇编文件中的函数(链接器会报错:unresolved external [email protected])。解决这一问题的方法是强制指定调用外部的汇编函数为__cdecl调用约定,即如果在C文件中调用汇编文件中的asm_rng_available函数,声明方式应如下:
    extern int          __cdecl asm_rng_available() ;
强制指定__cdecl调用约定后可以保证C编译器生成的asm_rng_available函数名和汇编文件中生成的函数名是一样的。
    64位体系结构中只有一个本机调用约定和一个__cdecl约定可以被编译器忽略,其他的调用约定都已经被清除,所以64位下的DDK编译器生成的函数名和汇编器生成的函数名是一致的,函数名不会带有后缀,因此不必修改。即如果在C文件中调用汇编文件中的asm_rng_available函数,声明方式如下:
    extern int          asm_rng_available() ;


2.SOURCE文件的修改
    由于Build命令不会自动关联汇编文件的目标文件,为了将编译后的汇编目标文件和C目标文件进行链接,可以在SOURCE文件的TARGETLIBS宏中进行指定。具体方法如下:
    TARGETLIBS=.\instr_32.obj
这样就可以使用Build命令直接对所有目标文件进行链接了。
3.64位和32位中一些系统调用的差异
    64位DDK中的lib库并没有导出KeInitializeSpinLock, KeQueryInterruptTime函数,因此不能直接调用,必须通过MmGetSystemRoutineAddress得到函数的指针,利用函数指针进行调用。
驱动中调用汇编函数
    下面对整个过程进行完整的描述:
    1. 将汇编文件和驱动程序的C文件放在同一个目录下(简称驱动目录);
    2. 修改SOURCE文件,在SOURCE文件中添加TARGETLIBS = *.obj,其中*表示汇编文件的文件名(不加扩展名);
    3. 在驱动程序C文件中声明外部汇编文件的函数名,如果是32位体系结构,声明时需强制指定汇编函数为__cdecl调用约定;
    4. 编译汇编文件,在驱动目录下生成obj目标文件(编译器可以采用nasm或者masm);
    5. 在DDK编译环境下对整个驱动工程执行Build命令,从而生成.sys文件。
OK...

 

因为对于C语言,在vs2005.net环境下可直接进行64位平台的编译,其中注意事项已有许多文章涉及,这里不再复述。

A、而对于汇编语言,首先要注意,必须为纯汇编格式(*.asm文件)或intrinsic指令格式。其次,在64位平台上,不建议使用nasm编译器(我没找到其对于64位编译的相关支持),而建议采用yasm,这个汇编编译器是在nasm的基础上产生的,可以说对nasm的功能兼容,并且支持64位编译,详细地介绍及相关下载见:http://www.tortall.net/projects/yasm/

说明:可以使用Visual Studio中自带的ml64.exe对64位汇编源文件进行编译:

64位驱动中加入汇编代码

示例图:

64位驱动中加入汇编代码

B、x86-64较x86-32多了8个通用寄存器,而且,每个通用寄存器都是64位宽,它们是:
rax,rbx,rcx,rdx,rsi,rdi,rsp,rbp
r8,r9,r10,r11,r12,r13,r14,r15

同时,x86-64全面支持x86-32和x86-16的通用寄存器:
eax,ax,al,ah,
ebx,bx,bl,bh,
但是,在对寄存器进行入/出栈操作时只能对相应的64位寄存器入/出栈,即:
( Instructions that modify the stack (push, pop, call, ret, enter, and leave) are implicitly 64-bit. Their 32-bit counterparts are not available, but their 16-bit counterparts are. Examples in NASM syntax: 
    push eax  ; illegal instruction
    push rbx  ; 1-byte instruction
    push r11  ; 2-byte instruction with REX prefix)

C、关于x64的调用约定:

在设计调用约定时,x64 体系结构利用机会清除了现有 Win32 调用约定(如 __stdcall、__cdecl、__fastcall、_thiscall 等)的混乱。在 Win64 中,只有一个本机调用约定和 __cdecl 之类的修饰符被编译器忽略。除此之外,减少调用约定行为还为可调试性带来了好处。

您需要了解的有关 x64 调用约定的主要内容是:它与 x86 fastcall 约定的相似之处。使用 x64 约定,会将前 4 个整数参数(从左至右)传入指定的 64 位寄存器:

RCX: 1st integer argument第一个参数在RCX寄存器存放
RDX: 2nd integer argument第二个参数在RCX寄存器存放
R8: 3rd integer argument第三个参数在RCX寄存器存放
R9: 4th integer argument第四个参数在RCX寄存器存放

前 4 个以外的整数参数将传递到堆栈。该指针被视为整数参数,因此始终位于 RCX 寄存器内。对于浮点参数,前 4 个参数将传入 XMM0 到 XMM3 的寄存器,后续的浮点参数将放置到线程堆栈上。

更进一步探究调用约定,即使参数可以传入寄存器,编译器仍然可以通过消耗 RSP 寄存器在堆栈上为其预留空间。至少,每个函数必须在堆栈上预留 32 个字节(4 个 64 位值)。该空间允许将传入函数的寄存器轻松地复制到已知的堆栈位置。不要求被调用函数将输入寄存器参数溢出至堆栈,但需要时,堆栈空间预留确保它可以这样做。当然,如果要传递 4 个以上的整数参数,则必须预留相应的额外堆栈空间。

 

让我们看一个示例。请考虑一个将两个整数参数传递给子函数的函数。编译器不仅会将值赋给 RCX 和 RDX,还会从 RSP 堆栈指针寄存器中减去 32 个字节。在被调用函数中,可以在寄存器(RCX 和 RDX)中访问参数。如果被调用代码因其他目的而需要寄存器,可将寄存器复制到预留的 32 字节堆栈区域中。图 6 显示在传递 6 个整数参数之后的寄存器和堆栈。

64位驱动中加入汇编代码

图 6 传递整数

对这里的参数压栈,需要说明的是(对于整形参数):
1、第四个参数后的参数按顺序反向压栈(8字节)
2、为前四个参数预留的32字节占空间是固定的、预分配的,不用被调用函数的编写者负责,也就是说,不管你是否用到,这部分栈空间都占用了,rsp所指向的位置如上图。
所以对于上面的例子,如果在被调用函数重要访问p5,p6正确的方法是:
mov eax,[esp+32+8];p5
mov ebx,[esp+32+16];p6

 D、一个很特别的寄存器 rip,相当于x86-32的eip. 在x86-32是不可直接访问的,如mov eax,eip是错的,但在x86-64位下却可以,如 mov,rax,qword ptr [rip+100]是对的。而且,它除了是个程序计数器外,也是个“数据基地址”,有此可见,它现在是身兼两职!为什么在x86-64位下要用rip做访问数据的基地址呢?因为,在x86-64下,DS,ES,CS,SS都没有实际意义了,也就是说,它们不再参与地址计算,只是为了兼容x86-32。FS,GS还是参与地址计算,它们两个和x86-32的意义相同。由于DS,ES,CS,SS没有意义了,所以在编译动态库时,符号变量是不允许直接出现在汇编代码中的,而必须与rip一起使用,即
mov rax,[symb wrt rip]或者lea rbx,[symb wrt rip]

参考网页:
AMD64 Architecture ---http://www.tortall.net/projects/yasm/wiki/AMD64
开始进行 64 位 Windows 系统编程之前需要了解的所有信息---http://www.microsoft.com/china/MSDN/library/Windev/64bit/issuesx64.mspx?mfr=true
The history of calling conventions, part 5: amd64---http://blogs.msdn.com/oldnewthing/archive/2004/01/14/58579.aspx


 

Trackback: http://tb.blog.****.net/TrackBack.aspx?PostId=1371665

 

下面给出一个示例代码:

汇编中实现一个加法函数,实现四个参数传入,分别对应上面介结的RCX第一个参数,RDX第二个参数,R8第三个参数,R9第四个参数

x64asm.asm文件名

.code
AddSub proc
push rcx

add rcx,rdx
add rcx,r8
add rcx,r9
mov rax,rcx

pop rcx
ret
AddSub endp
end

 

在驱动中调用汇编代码中提供的加法函数:AddSub

x64AsmDriver.c源文件名

#include <ntddk.h>

extern int AddSub(PVOID, PVOID, PVOID, PVOID);

VOID DriverUnload( IN PDRIVER_OBJECT DriverObject )
{
KdPrint(("DriverUnload OK!\n"));
}

#pragma code_seg("INIT")
NTSTATUS
DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING pRegPath
)
{
ULONG32 nResult = 0;
ULONG32 a = 10;
ULONG32 b = 20;
ULONG32 c = 30;
ULONG32 d = 40;

DriverObject->DriverUnload = DriverUnload;

nResult = AddSub(a, b, c, d);
KdPrint(("nResult = %d\n", nResult));

return STATUS_SUCCESS;
}

 

说明:关于驱动Source文件配置:将汇编产生的OBJ文件加到这里TARGETLIBS=.\x64Asm.obj

TARGETNAME=x64AsmDriver
TARGETTYPE=DRIVER
TARGETLIBS=.\x64Asm.obj


SOURCES=x64AsmDriver.c

相关文章