bash和gcc都能运行了,离“可用的系统”又进了一步。今天整理了下代码,放到了google code 上,有兴趣的都可以下载下来看。要是有谁对这也感兴趣,可以在下面留言,一起来玩。
如果把讨论范围缩小到x86平台,那么linux和windows的区别,至少在用户态层面的区别,比我们想象的要小很多,所以事实上如果你真想干的话,在windows上实现各类*nix特性并没有想象中那么困难。反过来说,在*nix上实现windows特性也完全能做到。这不是随口一说,前者有cygwin, coLinux,后者有wine,都是成熟的项目。我这个东西跟cygwin的区别前面已经说过很多次了,跟coLinux的区别在于,coLinux事实上还是有一层虚拟层的,它会在windows内核里加载一个硬件虚拟层,然后在这虚拟层上跑一个*真正的*linux内核,大致的架构可以参考这篇文章。而LINE不需要任何虚拟层,它是在windows内核里*直接*实现linux的系统调用,(当然,这是我的最终目标,目前还是要过cygwin这一模拟层的)。考虑到linux的程序基本是在glibc之上运行的,而glibc是纯用户态的东西,与内核的交流全部通过系统调用,如果我们真的能在windows内核里把linux系统调用全部实现,那么glibc压根就不知道自己是跑在windows上还是linux上。
LINE的核心部件有两个:内核态的int 80响应函数,以及用户态的elf loader。目前int 80响应函数的实现非常简单,20来行汇编就搞定了,因为是第一阶段嘛,所有的东西全在用户态实现,把逻辑搬到内核的工作是下一阶段的事情。大致的代码如下:
1 _InterruptHandler proc
2
3 ; Check for SYSCALL_LINEXEC_HANDLER
4 CMP EAX, 0DEADBEEFh ;LINE.exe运行的第一件事情就是设置用户态响应函数的地址,以便从系统调用返回的时候可以转到这里。此一次int 80的eax设置成DEADBEEF,这个值不是任何系统调用号
5 JNE reflect_syscall
6
7 MOV DS:_syscallHandlerPtr, EBX ;保存用户态响应函数的地址
8
9 IRETD
10
11
12 reflect_syscall:
13 PUSH EAX
14
15 ; simple sanity check
16 MOV EAX, DS:_syscallHandlerPtr
17 CMP EAX, 0
18 JE no_handler
19
20 PUSH EBX
21
22 MOV EBX, DWORD PTR [ESP+8] ; 用户态程序调用int 80时,该指令自动帮你保存的返回地址
23 MOV DWORD PTR [ESP+8], EAX ; 把返回地址替换成我们在用户态的响应函数
24
25 MOV EAX, DWORD PTR [ESP+8+12] ;
26 SUB EAX, 4
27 MOV DWORD PTR [ESP+8+12], EAX ;
28 MOV DWORD PTR [EAX], EBX ; 以上几步把旧的返回地址保存起来,用户态响应函数做完事情后,就跳转回这个地址
29
30 POP EBX
31
32 JMP exit_handler
33
34 no_handler:
35 POP EAX
36 PUSH -38 ; -38 == ENOSYS
37
38 exit_handler:
39 POP EAX
40 IRETD
41
42 _InterruptHandler endp
43
44 End
如上所示,这段汇编把原有的调用流程全改了。原有的中断流程大概是这样的:程序调用int 80中断—>cpu自动将EIP压栈—>(1)进入内核做事—>做完后内核调用iretd—>返回用户态—>自动把EIP恢复。改完后的中断流程大概是这样的:程序调用int 80中断—>cpu自动将EIP压栈(我们假设为地址1)—>进入内核—>(2)更改栈上保留的EIP,改成用户态的响应函数(我们假设为地址2)—>再保存地址1—>iretd返回用户态—>恢复EIP,此时恢复的是地址2—>转到地址2做事情—>(3)做完后返回地址1。对于(2)和(3)之间的事情,int 80发起程序完全不知道,它只知道中断前保存的是哪个指令,最后回来执行的还是那条指令。如果第二阶段完成后,(2)和(3)之间的东西也完全没必要存在了,它们会移到(1)这个地方。
接下来我们说elf loader。我们知道一个进程的创建过程基本是这样的:fork创建新进程—>子进程里调用exec*函数,用目标程序替换现有程序。而替换过程基本是这样的:load elf—>解析import table—>加载所有依赖的库—>调整各类库的导出函数地址—>找到elf的入口函数地址—>跳转。这一整套步骤在LINE里都重新实现了一遍,因为windows上只有PE loader,根本认不出elf格式的东西,所以加载elf文件的工作就得我们自己来。假设我们目前运行的是bash程序,然后敲了一个命令ls –al,这个过程在linux上大概是这样的:fork—>父进程等待,子进程exec*( “ls”, argv, env) //argv[0] = ls, argv[1] = –al。在LINE上则变成了这样:fork—>父进程等待,子进程exec*( “Line.exe”, argv, env ) // argv[0] = line, argv[1] = ls, argv[2] = –al。也就是说,原本是新开“ls –al”这样一个进程,现在变成新开”line ls –al”这个进程,这样所有的进程都变成由line来加载了。至于elf加载的具体细节,不管你是看linux内核源码,还是看ld-linux这个库的源码,都能有超详细的解释,这里就不多说了。
最后还有两个比较棘手的问题:内存布局和路径。不管我们怎么弄,line.exe还是一个PE文件,由windows负责加载,加载完后有一些内存块就已经被占用了(一般来说是0x40000000及以上的地址)。要是我们的elf文件需要这些地址,那事情就麻烦了。幸运的是elf的首地址一般是0x08040000,离0x40000000还远着呢,足够我们用了。但事实上即使是0x40000000以下的地址,有一些也是windows标记为不可执行的,我们必须在load elf文件之前将需要的内存块重新标记可执行。
而路径绝对是windows上最恶心人的地方(之一)。记得在之前一篇博客里我曾说过可以用windows中的namespace模拟单根的文件系统,这在第二阶段绝对是可行的,不过第一阶段因为全是在用户态实现所以会比较困难。目前我的解决方法是把所有的绝对路径都改成相对路径。比如我们的line.exe程序放在c:/line目录里,那么当linux程序访问/bin目录时,我会把当前目录附加在该目录前面,变成c:/line/bin目录。同样的/lib目录则成了c:/line/lib目录,以此类推。目前为止这套机制运行的还算不错。至于/proc,/dev这些虚拟路径,目前还没有实现,不过借助cygwin应该也还是能做。转换路径的函数如下:
1 void change_path_to_relative(char* des, char* src)
2 {
3 char root_path[MAX_PATH] = {0};
4 char* slash = NULL;
5 if( !src || !*src || !des){
6 return;
7 }
8 if( src[0] != '/' ){
9 strcpy(des, src);
10 return;
11 }
12 strcpy(root_path, linexec_exe); //我们假设line所在的目录在启动的时候就已经保留
13 slash = strrchr(root_path, '/');
14 if( !slash ){
15 strcpy(des, src);
16 return;
17 }
18 *slash = '\0';
19 strcpy(des, root_path);
20 strcat(des, src);
21 return;
22
23 }
最后附上编译步骤以及运行截图若干,一个编译好的包连上我从ReadHat 6.0上拷出来的库压缩后有200M+,等我找个免费的网络空间再上传放出。
编译驱动:
1. 下载安装WDK
2. 运行WDK编译环境,进入源代码的src目录
3. 进入int80目录
4. 运行build –g –c
5. 将生成的int80.sys考入i386目录
6. 用管理员权限运行install.bat
编译程序:
1. 下载安装cygwin
2. 运行cygwin,进入源代码src目录
3. 运行make
4. 不出意外的话,会在src目录生成几个dll和exe,全拷贝到其他目录里,比如c:/line
5. 从cygwin的安装目录里找到cygwin1.dll, cyggcc_s-1.dll这两个文件,拷贝到你的目录里
6. 找旧一点的linux程序和库(确保没有NPTL,这个我还没想到办法实现。。。),考到你的目录里,保留目录结构,确保根目录就是你的目录
7. 运行cmd,进入你的目录,执行”line.exe bash “命令
截图
玩的愉快