有没有一种方法可以避免RTL(从右到左)?

5

我一直在使用 Delphi 进行一个不允许使用 RTL 的 dll 相关的操作。

在分析 PE(Portable Executable)文件格式后,我发现所有的 PE 文件都有一个“入口点”(Entry Point)。这是 Windows 在加载模块(exe 或者 dll)后第一个调用的函数。

该函数的名称以及其隐含参数取决于所属 PE 的类型:

  • Dynamic Link Library (*.dll): DllMain

    function DllMain(hinstDLL: HINST; fdwReason: DWORD; 
          lpvReserved: Pointer): BOOL; stdcall;
    
  • Executable (*.exe): WinMain

    function WinMain(hInstance: HINST; hPrevInstance: HINST; 
          lpCmdLine: LPSTR; nCmdShow: Integer): Integer; stdcall;
    
  • Device Driver (*.sys): DriverEntry

    function DriverEntry(DriverObject: PDriverObject; 
          RegistryPath: PUnicodeString): NTSTATUS; stdcall;
    
在Delphi中,这个“入口点”指的是位于主项目文件中的代码。
对于DLL来说,可以通过LoadLibrary读取传递给我们的“DllMain”的参数。如果在EntryPointAddress处设置断点:

enter image description here

您可以在堆栈上看到三个参数:

enter image description here

你所需要做的就是捕获它们:

library Project1;

function DllMain(hinstDLL: HINST; fdwReason: Cardinal; lpvReserved: Pointer): Integer; stdcall;
begin
    Result := 1; //Windows uses FALSE=0, TRUE=1. Delphi uses False=0, True=-1
end;

begin
    //Code in here is run during DllMain.
    //i.e. DllMain does not return until this code completes.
    asm
        { Get the parameters to DllMain that Windows passed to us:
                [ESP+4] hinstDLL
                [ESP+8] fdwReason
                [ESP+12] lpvReserved
        }
        MOV eax, [ESP+12];  //push lpvReserved onto the stack
        PUSH eax;
        MOV eax, [ESP+8];  //push fdwReason onto the stack
        PUSH eax
        MOV eax, [ESP+4];  //push hinstDLL onto the stack
        PUSH eax;

        CALL DllMain;       //Call our DllMain function
                            //DllMain leaves BOOL result in EAX
   end;
end.

但是有一个RTL

问题在于里面不仅仅只有我的代码。编译器会在项目文件中的我的代码块之前之后插入隐藏的代码:

enter image description here

实际上,真正的 DllMain 代码包含:

function DllMain(hinstDLL: HINST; fdwReason: Cardinal; lpvReserved: Pointer): LongBool; stdcall;
begin
    SysInit._InitLib(InitTable, hinstDLL, fdwReason, lpvReserved);

    asm
        MOV eax, [ESP+12];  //push lpvReserved onto the stack
        PUSH eax;
        MOV eax, [ESP+8];  //push fdwReason onto the stack
        PUSH eax
        MOV eax, [ESP+4];  //push hinstDLL onto the stack
        PUSH eax;

        CALL DllMain;       //Call our DllMain function
                            //DllMain leaves BOOL result in EAX
   end;

   System._Halt0;
end;

现在,这个对_InitLib的前置调用会对尝试提取hinstDLLfdwReason的值造成一些破坏;但这不是一个无法克服的问题(例如,你仍然可以在EBP+8+12+16找到它们)。
但我的问题是RTL链接到的代码并不总是可用的。查看导入目录表,你可以看到它需要:
- user32.dll(例如MessageBoxA) - kernel32.dll(例如VirtualAllocVirtualFreeCloseHandle
我能避免使用RTL吗?
有没有编译器选项或定义可以剥离System._InitLibSystem._Halt0的所有内容?或者让编译器不将它们放入:
function DllMain(hinstDLL: HINST; fdwReason: Cardinal; lpvReserved: Pointer): LongBool; stdcall;
begin
   SysInit._InitLib(InitTable, hinstDLL, fdwReason, lpvReserved);

   //...
   System._Halt0;
end;

很明显,编译器在创建它们时具有特殊的智能。而且,如果是库或应用二进制文件,会隐含地放置在EntryPointProcedure中的内容也会发生变化。

额外闲聊:缺失的WinMain参数案例

WinMain入口点文档要求传递四个参数:

function WinMain(hInstance: HINST; hPrevInstance: HINST; 
          lpCmdLine: LPSTR; nCmdShow: Integer): Integer; stdcall;
  • hInstance: HINSTANCE // 应用程序实例句柄
  • hPrevInstance: HINSTANCE // 前一个应用程序实例句柄
  • lpCmdLine: LPSTR // 命令行参数字符串
  • nCmdShow: Integer // 窗口显示状态

这就是为什么我无法弄清楚为什么参数没有传递给EntryPointFunction的原因:

enter image description here

我不断地在堆栈中进行调试。我尝试了其他调试器,但Windows没有将适当的参数传递给入口点函数。然后我找到了答案

操作系统以无参数的方式调用该函数

真正的 Windows .exe 入口点函数是:
DWORD CALLBACK RawEntryPoint(void);

即:

function RawEntryPoint(): DWORD; stdcall;

如果参数没有传递到原始入口点,那WinMain的参数从哪里来呢? 语言启动代码通过向操作系统询问来获取它们: 可执行文件的实例句柄来自GetModuleHandle(NULL) 命令行来自GetCommandLine nCmdShow来自GetStartupInfo hPrevInstance始终为NULL 所以这就解释了。

你将不得不放弃动态数组、字符串、异常和许多其他东西。你准备好了吗?你是在尝试制作一个WinRT DLL吗? - David Heffernan
@DavidHeffernan .exe -> WinMain(...),**.dll** -> DllMain(...),**.sys** -> DriverEntry(...)。我不想提及内核模式设备驱动程序开发,因为它可能会混淆问题。(“它是一种dll。”一个驱动程序就是一个带有导出和入口点的DLL) - Ian Boyd
2
我认为你最好使用FPC来完成这个任务:http://wiki.freepascal.org/Target_NativeNT 你应该明确说明你的目标是驱动程序。这样会让一切变得更加清晰明了。 - David Heffernan
它并不存在。你需要自己破解SystemSysInit。如果你想要Pascal设备驱动程序,FPC是你最好的选择。 - David Heffernan
@DavidHeffernan 如果编译器中没有选项或者无法设置define,那么以答案的形式表述。当正确答案是“无法完成”时,说出来并不可耻! - Ian Boyd
显示剩余2条评论
3个回答

9
KOL仍然活跃,并且官方网站http://kolmck.net(由我维护)包含了有关如何重写系统单元的示例。

我已经下载了它。我看到了FAQ问题(“是否可以在不卸载VCL的情况下使用KOL(MCK)?如果我安装了KOL(MCK),是否可以在不重新安装Delphi的情况下使用VCL?”)。我已经阅读了read1st.txt,但我不知道如何使用它。似乎您成功地替换了整个RTL;这是隐藏每个项目的RTL的一步。但是,我应该包含什么以使编译器/链接器无法找到/使用RTL? - Ian Boyd
在2016年8月,该链接似乎已经失效。也许链接应该指向这里:https://sourceforge.net/projects/keyobjectslibrary/ - Wouter van Nifterick

3

如果你想要实现自己的愿望,使用原始编译器/RTL是不可能的。编译器需要 SystemSysInit 单元的存在,并确实使用这些单元为许多语言功能提供运行时支持。例如,字符串是通过在 System 单元中实现支持函数来实现的。

如果您提供可以被编译器接受的替代 SystemSysInit 单元,则可以消除对用户模式模块的依赖。但是,这并不适合新手。

由于您似乎希望编写内核模式驱动程序代码,我建议使用 FPC。


由于我需要在构建后执行操作,将 IMAGE_OPTIONAL_HEADER.SubsystemIMAGE_SUBSYSTEM_WINDOWS_GUI 更改为 IMAGE_SUBSYSTEM_NATIVE,我可以利用这个机会来 nop 调用。我只是希望在某个时候,Bo...Imp...Cod...Embarcadero,通过他们的新操作系统目标,暴露出不使用 Windows dlls 的方法。 - Ian Boyd

2

当一个DLL被加载并且它的主DPR代码开始运行时,你可以从RTL的全局变量SysInit.HInstance中获取hinstDLL值,并且你隐式地知道fdwReason值必须是DLL_PROCESS_ATTACH。唯一无法在DLL_PROCESS_ATTACH期间检索的值是lpvReserved值,因为RTL会忽略它而不是将其保存在某个地方。然而,如果你想让你的代码对其他fdwReason值做出反应,并在这些其他原因状态下接收lpvReserved值,你只需要分配一个DllProcEx回调,RTL从其内部的DllMain入口点调用它,例如:

library Project1;

{$R *.res}

procedure MyDllMain(Reason: Integer; Reserved: Pointer);
begin
  // use SysInit.HInstance, Reason, and Reserved as needed...
  case Reason of
    //...
  end;
end;

begin
  DllProcEx := @MyDllMain;
  // The RTL does not expose access to lpvReserved here because 
  // DllProcEx has not been assigned yet when the RTL processes
  // DLL_PROCESS_ATTACH before calling unit initializations. If
  // you need lpvReserved here, you will have to manually pull
  // it from the call stack directly...
  DllProcEx(DLL_PROCESS_ATTACH, nil);
end.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接