winux项目[1]是我五年前参加学校科技创新申请的项目,用时一年实现了PE Loader以及kernel32.dll里面3个Win32 API的移植工作,后来因为项目结题加上要忙其他事情就把后续开发工作搁置了。最近整理以前的代码的时候发现关于这个项目除了结题时提交的一篇水paper之外再没写过任何技术资料,所以趁着相关知识还没全忘赶紧写篇手记留作纪念。

winux的名字取自Windows+Linux之意。在立项的当时,Linux下已经有了较为成熟的Wine项目来解决在Linux上运行Win32可执行文件的问题。“Wine是Windows应用软件与Linux内核之间的适配层,体现为一个Wine服务进程(wineserver)和一组动态连接库(相当于Windows的众多DLL)。”[2]Wine是运行在User Mode的程序。当时,浙大的毛德操老师也在进行一个名为LONGENE的项目[3],旨在实现一个运行在Kernel Mode的兼容内核。至于两种实现方式的具体比较和孰优孰劣的讨论这里就不展开了,毕竟winux项目诞生的本意也只是为了好玩顺便学习相关知识,采用了毛德操老师的设计思路,最终实现了一个非常简(jian)易(lou)的PE Loader,顺带导出了kernel32.dll里面3个Win32 API:ExitProcess,GetStdHandle,WriteConsoleA。需要注明的是本项目仅仅参考了LONGENE的项目的设计方案,所有的实现代码均为独立完成。项目的源文件和makefile可在GitHub上找到[1],项目演示地址可见参考资料[4]。

开发环境

Host OS: Windows XP
Client OS (in VMware): Ubuntu 8.04 (Linux 2.6.24) (注:这是2012年我对项目进行重新编译和测试时使用的Linux版本)
IDE:Source Insight

defines.h

首先我们需要对PE Loader中使用到的各种结构体和宏进行声明。所有的声明都在defines.h文件里,不需要展开讨论了。

kernel module的加载与卸载

关于如何编写Linux的kernel module,有无数资料可以参考。我们需要实现winux_init和winux_exit函数:

static int winux_init(void)
{
    printk("++++++++++++++++++++\n");
    pe_format.module = THIS_MODULE;
    pe_format.load_binary = load_pe_binary;
    pe_format.load_shlib = load_pe_library;
    pe_format.core_dump = pe_core_dump;
    pe_format.min_coredump = PAGE_SIZE;
    pe_format.hasvdso = 0;
    register_binfmt(&pe_format);
    return 0;
}

static void winux_exit(void)
{
    unregister_binfmt(&pe_format);
    printk("--------------------\n");
}

module_init(winux_init);
module_exit(winux_exit);

我们只需要实现load_pe_binary函数即可,load_pe_library和pe_core_dump两个函数不必搭理。

static int load_pe_binary(struct linux_binprm *bprm, struct pt_regs *regs)

load_pe_binary函数的实现方式可以参考Linux内核中现有的load_elf_binary函数。

首先我们判断image的Magic Number:

// Load IMAGE_DOS_HEADER
pe_dos_header = (IMAGE_DOS_HEADER*)bprm->buf;
// Is it a PE executive file?
if (pe_dos_header->e_magic != 0x5a4d || !pe_dos_header->e_lfanew)
{
    printk("NOT PE 1\n");
    goto err_not_pe;
}

如果没问题,我们读取整个PE Header,判断文件是不是合法的PE image。

读取成功后,我们需要设置image_base,即加载整个exe image的内存基址:

image_base = pe_optional_header->ImageBase;

设置栈顶位置:

retval = setup_arg_pages(bprm, WIN32_STACK_LIMIT + WIN32_LOWEST_ADDR, EXSTACK_DISABLE_X);

遍历各个section并加载到内存中:

if (pe_file_header->NumberOfSections > 0)
{
    pe_section_header = (IMAGE_SECTION_HEADER*)kmalloc(sizeof(IMAGE_SECTION_HEADER) * pe_file_header->NumberOfSections, GFP_KERNEL);
    if (!pe_section_header)
    {
        retval = -ENOMEM;
        printk("failed to kmalloc for pe_section_header!\n");
        goto out;
    }
    retval = kernel_read(bprm->file, pe_dos_header->e_lfanew + sizeof(IMAGE_NT_HEADER), (char*)pe_section_header, sizeof(IMAGE_SECTION_HEADER) * pe_file_header->NumberOfSections);
    if (retval < 0)
        goto err_read_section_header;

    for (i = 0, pe_ppnt = pe_section_header; i < pe_file_header->NumberOfSections; i++, pe_ppnt++)
    {
        ...
    }
}

在这之后我们的exe就基本加载好了,接下来设置代码段、数据段和bss段的起止地址,并设置代码段第一行代码的入口点pe_entry:

pe_entry = pe_optional_header->AddressOfEntryPoint + image_base;
start_bss += image_base;
end_bss += image_base;
start_code += image_base;
end_code += image_base;
end_code += 0x200;
start_data += image_base;
end_data += image_base;
printk("------->\nentry: %#x\n", pe_entry);
set_binfmt(&pe_format);
compute_creds(bprm);

current->flags &= ~PF_FORKNOEXEC;
current->mm->start_code = start_code;
current->mm->end_code = end_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_brk = start_bss;
current->mm->brk = end_bss;

printk("start_code: %#x, end_code: %#x\n", current->mm->start_code, current->mm->end_code);
printk("start_data: %#x, end_data: %#x\n", current->mm->start_data, current->mm->end_data);
printk("start_bss: %#x, end_bss: %#x\n", current->mm->start_brk, current->mm->brk);
printk("start_stack: %#x\n", current->mm->start_stack);

if (start_bss > image_base && end_bss > image_base && start_bss <= end_bss)
{
    printk("set_brk with value 0\n");
    pe_set_brk(start_bss, end_bss);
    pe_padzero(start_bss);
}

下面是重头戏,处理exe的导入表,加载对应的dll image,然后根据dll的导出表计算对应函数的地址然后修改导入表的对应项,之后将修改好的导入表数据写入到user内存空间。这段代码比较长和复杂就不贴了,请自行查看源文件。

最后调用start_thread(regs, pe_entry, bprm->p)启动主线程,CPU届时会开始运行pe_entry指向的代码。

编译、链接与效果演示

生成kernel module的makefile:

obj-m += winux.o
EXTRA_CFLAGS = -w
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)

all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules

clean:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

成功后会生成winux.ko文件。可以通过insmod winux.ko命令加载。加载后可以通过rmmod winux.ko命令卸载。

当前的演示exe及dll都很简单,都是通过Win32汇编语言生成的。

exe中的代码段:

    .code
start:
    push 0
    push offset write
    push len
    push offset txt

    push STD_OUTPUT_HANDLE
    call GetStdHandle

    push eax
    call WriteConsoleA

    push 0
    call ExitProcess
    ret

    end start

build需要的Makefile已经包含在项目中,直接使用nmake即可。

dll目前只导出了3个函数:

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; dll 的入口函数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DllEntry proc

    mov eax, 1
    ret

DllEntry Endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ExitProcess proc uExitCode

    mov ebx, uExitCode
    mov eax, 1
    int 80h

ExitProcess endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
GetStdHandle proc nStdHandle

    mov eax, 1
    ret 4

GetStdHandle endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
WriteConsoleA proc hConsoleOutput, lpBuffer, nNumberOfCharsToWrite, lpNumberOfCharsWritten, lpReserved

    pushad
    mov edx, nNumberOfCharsToWrite        ; 参数三:字符串长度
    mov ecx, lpBuffer                     ; 参数二:要显示的字符串
    mov ebx, hConsoleOutput               ; 参数一:文件描述符(stdout)
    mov eax, 4
    int 80h
    mov eax, lpNumberOfCharsWritten
    mov edx, nNumberOfCharsToWrite
    mov [eax], edx
    popad
    mov eax, 1
    ret 20

WriteConsoleA endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    End DllEntry

build方法和exe一样,成功后会生成kernel32.dll文件,需要拷贝到Linux下的/usr/dlls/路径下才能被winux.ko找到(硬编码神马的……拖)。

一切就绪后并成功加载winux.ko后,只需要像运行其他可执行文件一样,直接敲./test.exe便可以看到效果了!

尾声

这个项目目前几乎没有任何实用价值,主要还是学习和演示基本技术的成分居多。欢迎感兴趣的小伙伴们为项目继续添砖加瓦。直接拿走当作自己的作业或者课程设计什么的可是不行的哦ԅ(¯﹃¯ԅ)

参考资料:
[1] winux on GitHub
[2] 漫谈Wine之一:WINE的系统结构
[3] Linux兼容内核网站
[4] Winux

» 转载请注明来源及链接:未来代码研究所

Related Posts:

5 Responses to “winux项目手记——Linux内核中简易Win32可执行文件加载器(PE Loader)的设计与实现”

  • David Huang says:

    我朝这几年搞自主研发操作系统的风头比较大,兼容windows应用这个话题在国内大学应该也会比较热门。。

    • 暗影吉他手 says:

      如果要用这个方式实现Win32 API兼容的话工作量巨大,你看我都没敢碰GUI相关的API,就移植了个console的函数。。。我估计要是大学搞这个肯定最后就装个WINE然后结题了

  • vicky says:

    偶然来到这里,请问一下,你就是win8的哔哩哔哩程序员?

  • xieaoran says:

    好主意,果断拿去交软设2作业(逃)

Leave a Reply

World Line
Time Machine
Friendly Links
Online Tools