Wine源码中添加新的DLL模块
1. 基础环境准备
编译环境:debootstrap 安装 debian bullseye
源码版本:Wine 9.0-rc4
基础环境搭建
2. 创建DLL模块目录
在dlls目录下新建一个文件夹:nfs
将amsi目录下的三个文件全部复制到nfs目录下:
main.c 文件内容中新加一个函数如下:
BOOLEAN WINAPI Test_In_CreateWindowEx( const WCHAR *classname, ULONG* style, ULONG* dwExStyle )
{
TRACE( "classname=%s, style=0x08x, style=0x08x \n", debugstr_w(classname), style, dwExStyle);
return TRUE;
}
spec文件改名为nfs.spec, 将上面实现的函数导出给外部调用,nfs.spec内容如下:
@ stdcall Test_In_CreateWindowEx(ptr ptr ptr)
Makefile.in是一个模块文件,用于生成makefile文件, 其示例内容如下:(IMPORTLIB是为了生成.a文件)
MODULE = nfs.dll # 生成windows动态链接库的名字
UNIXLIB = nfs.so # 生成Linux下的共享库的名字
IMPORTLIB = nfs # 生成静态库
SOURCES = \
main.c
3. 修改配置文件
将源码根目录下的 configure和configure.ac两个文件的权限改为可以编辑。
chmod 777 ./configure
chmod 777 ./configure.ac
打开configure.ac 文件找到dlls/amsi配置所在的行,按其样式,在他下方添加新的模块名
...
WINE_CONFIG_MAKEFILE(dlls/amsi)
WINE_CONFIG_MAKEFILE(dlls/nfs)
...
修改完成后,执行autoconf命令,重新生成configure文件,文件中会包含2处新加的模块变更:
ac_user_opts='
...
enable_nfs
...
wine_fn_config_makefile dlls/nfs enable_nfs
...
4. 验证配置文件
执行./configure
...
configure: Finished. Do 'make' to compile Wine.
运行成功后,在dlls/nfs目录下可以看到,一个名为Makefile的文件生成出来, 文件内容如下:
all:
all install install-lib clean i386-windows/main.o i386-windows/nfs.dll:
@cd ../.. && $(MAKE) dlls/nfs/$@
.PHONY: all install install-lib clean
5. user32模块中调用
如果要在user32模块中调用新加的DLL中的函数,编辑dlls/user32/Makefile.in文件,将nfs加到IMPORTS后。
加载nfs.dll
IMPORTS = $(PNG_PE_LIBS) gdi32 sechost advapi32 kernelbase win32u nfs
dlls/user32/win.c文件中, 声明一下Test_In_CreateWindowEx方法,然后在WIN_CreateWindowEx方法内就可以直接调用了。
5. winex11drv模块中调用
5.1 添加头文件
在winex11drv模块中调用自定义模块的函数时,需要添加函数的声明头文件test.h,然后将test.h文件放到wine代码中的include目录下,同时需要在include/Makefile.in中添加头文件,不然会编译出错,提示找不到头文件。
5.2 共享库中对外函数的实现
分两种调用情况:
- 当调用nfs模块中的函数是在如下三个文件中时
dllmain.c systray.c xdnd.c ,可以直接成功调用,因为这三个目标文件是生成在i386-windows中,也就是模拟windows的dll库,虽然他的名字叫winex11.drv,实际是一个dll. - ,在winex11drv中调用我们自定义的nfs时,当果在上述三个文件之外,会发现在link时会报错说找不到函数的实现,这是因为winex11drv 本身需要生成so库,如果在生成so库的源文件中,调用其它的模块时,实际上需要的是被调用模块的so文件,因此需要显示的将so文件加到winex11drv/makefile.in 文件的UNIX_LIBS后边。
UNIX_LIBS = -lwin32u $(X_LIBS) $(X_EXTRA_LIBS) $(PTHREAD_LIBS) -lm -lnfs
理论上这样添加后就可以调用到nfs.so中的方法了,但是添加完后还是会提示找不到实现,
使用nm来查看so中我们添加的函数时,可以看到函数是加进去了,但是修饰符号是小写的t,表示你编入so文件中的函数是仅限模块内部使用,其实在configure中可以看到, wine在编译时会默认加上下边的选项:
CFLAGS="$CFLAGS -fvisibility=hidden"
如果需要so中的函数定义在外部模块使用,需要显示的将函数声明为 attribute_((visibility (“default”))), 加好后重新编译,再用nm来查看生成的so文件时,可以看到编入so的函数的修饰符由小写的t变为大写的T了,这时就允许外部调用了。
5.3 添加系统级的封装函数
wine6开始代码中的dll是用来模拟windows调用的,实际执行时还是要运行对应的windows代码,因此dlls中的函数通常是fake的,也就是说需要建立一个dll中函数调用时,映射到对so中的linux函数的调用。
系统级别的函数有几个好处:
- 可自动生成声明函数;
- 执行效率优化后更高;
- 跨平台可移植性更好;
- 异常错误处理统一化;
举个例子:如果我们需要包装一个与文件操作相关的函数时,可以使用-syscall来封装不同操作系统下的不同文件操作方式,以及对路径的不同处理方式,而对上层调用方来说,是统一无差异的。
wine源码中的NtTerminateThread函数就使用了-syscall标识符,其代码实现如下:
NTSTATUS WINAPI NtTerminateThread( HANDLE handle, LONG exit_code )
{
unsigned int ret;
BOOL self;
SERVER_START_REQ( terminate_thread )
{
req->handle = wine_server_obj_handle( handle );
req->exit_code = exit_code;
ret = wine_server_call( req );
self = !ret && reply->self;
}
SERVER_END_REQ;
if (self)
{
server_select( NULL, 0, SELECT_INTERRUPTIBLE, 0, NULL, NULL );
exit_thread( exit_code );
}
return ret;
}
大部分-syscall 的方法是由wineserver来实现的,将复杂的逻辑和错误处理都包装了起来,让外部更容易使用。
接着说明关于so的文件生成:
如果要生成.so文件,单纯的添加UNIXLIB = nfs.so,是无法加入函数的实现的。
首选创建一个test_so.c,然后在文件中添加函数test_so_fun(),
为了让test_so.c生成的目标文件不放到i386-windows中,需要在文件顶部添加一段代码:
#if 0
#pragma makedep unix
#endif
然后将导出函数添加到spec文件中,增加-syscall修饰:
@ stdcall -syscall test_so_fun()
最后将 test_so.c 加到Makefile.in中;
这时执行如下编译命令:
configure && make -j16
会报错,提示找不到test_so_fun的实现,原因是: 生成的dll文件是在i386-windows目录生成的,然后包含了实现的目标文件 test_so.o 并不在 i386-windows中,而是在模块的根目录dlls/nfs下,是在 nfs.so 中包含了test_so_fun的实现。
那么如何才能编译通过,并使其调用时链接到nfs.so 中的实现?
通过对比可以发现,win32u模块中可以编译通过是因为有如下的代码:
//win32syscalls.h
#define ALL_SYSCALLS32 \
SYSCALL_ENTRY( 0x0000, NtGdiAbortDoc, 4 ) \
SYSCALL_ENTRY( 0x0001, NtGdiAbortPath, 4 ) \
SYSCALL_ENTRY( 0x0002, NtGdiAddFontMemResourceEx, 20 ) \
因此我们使用一下wine源码中tools目录下的 make_specfiles 在wine源码的根目录执行后,会通过spec文件中指定的函数,自动生成上述的定义。
接下来需要在dlls/nfs/main.c文件中添加引用代码:
void *__wine_syscall_dispatcher = NULL;
static unixlib_handle_t nfs_handle;
#ifdef _WIN64
#define SYSCALL_ENTRY(id,name,args) __ASM_SYSCALL_FUNC( id + 0x1000, name )
ALL_SYSCALLS64
#else
#define SYSCALL_ENTRY(id,name,args) __ASM_SYSCALL_FUNC( id + 0x2000, name, args )
DEFINE_SYSCALL_HELPER32()
ALL_SYSCALLS32
#endif
#undef SYSCALL_ENTRY
BOOL WINAPI DllMain( HINSTANCE inst, DWORD reason, void *reserved )
{
HMODULE ntdll;
void **dispatcher_ptr;
const UNICODE_STRING ntdll_name = RTL_CONSTANT_STRING( L"ntdll.dll" );
switch (reason)
{
case DLL_PROCESS_ATTACH:
LdrDisableThreadCalloutsForDll( inst );
if (__wine_syscall_dispatcher) break; /* already set through Wow64Transition */
LdrGetDllHandle( NULL, 0, &ntdll_name, &ntdll );
dispatcher_ptr = RtlFindExportedRoutineByName( ntdll, "__wine_syscall_dispatcher" );
__wine_syscall_dispatcher = *dispatcher_ptr ;
if (!NtQueryVirtualMemory( GetCurrentProcess(), inst, MemoryWineUnixFuncs,
&nfs_handle, sizeof(nfs_handle), NULL ))
__wine_unix_call( nfs_handle, 0, NULL );
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
这些都完成后,编译就可以通过了,编译出来的nfs.dll和nfs.so可以用nm来查看一下,都包含了test_so_fun
6. 新加模块的初始化
先简单说明一下启动流程:
loader模块: main函数: 加载ntdll.so -> 执行dlsym( handle, "__wine_main" ) -> 调用dlls/ntdll模块中的 __wine_main
dlls/ntdll 模块: __wine_main函数: check_command_line -> loader_exec -> start_main_thread -> load_ntdll
load_ntdll模块启动后,它会负责加载其它的dll模块,大部分模块在被ntdll加载时会调用模块内的__wine_unix_call_funcs数据中的函数指针,数据中的函数指针对应的函数都会顺序执行。类似如下这种:
const unixlib_entry_t __wine_unix_call_funcs[] =
{
init1,
init2,
init3,
};
这个__wine_unix_call_funcs数组之所以能被调用是因为每一个dll模块dllmain的执行时触发的,具体流程如下:
可以将需要初始化的操作放到相关的init中,你可以自己尝试一下如何将新加的模块也放入ntdll中来启动。