我们需要模拟的64位Linux程序,它传参使用了System V AMD64 ABI标准, 先把参数按RDI, RSI, RDX, RCX, R8, R9的顺序设置,如果有再多参数就放在栈中.
而64位的Windows传参使用了Microsoft x64 calling convention标准, 先把参数按RCX, RDX, R8, R9的顺序设置,如果有再多参数就放在栈中, 除此之外还需要预留一个32字节的影子空间.
如果我们需要让Linux程序调用Windows程序中的函数, 需要对参数的顺序进行转换, 这就是上面的汇编代码所做的事情.
转换前的栈结构如下
[原返回地址 8bytes] [第七个参数] [第八个参数] ...转换后的栈结构如下
[返回地址 8bytes] [影子空间 32 bytes] [第五个参数] [第六个参数] [第七个参数] ...因为需要支持不定个数的参数, 上面的代码用了一个thread local变量来保存原返回地址, 这样的处理会影响性能, 如果函数的参数个数已知可以换成更高效的转换代码.
在设置好动态链接的函数地址后, 我们完成了构想中的第4步, 接下来就可以运行主程序了
// 获取入口点 std::uint64_t entryPointAddress = *reinterpret_cast<const std::uint64_t*>(elfHeader.e_entry); void(*entryPointFunc)() = reinterpret_cast<void(*)()>(entryPointAddress); std::cout << "entry point: " << entryPointFunc << std::endl; std::cout << "====== finish loading elf ======" << std::endl; // 执行主程序 // 会先调用__libc_start_main, 然后再调用main // 调用__libc_start_main后的指令是hlt,所以必须在__libc_start_main中退出执行 entryPointFunc();
入口点的地址在ELF头中可以获取到,这个地址就是_start函数的地址, 我们把它转换成一个void()类型的函数指针再执行即可,
至此示例程序完成了构想中的所有功能.
执行效果如下图
这份示例程序还有很多不足, 例如未支持32位Linux程序, 不支持加载其他Linux动态链接库(so), 不支持命令行参数等等.
而且这份示例程序和Bash On Windows的原理有所出入, 因为在用户层是无法模拟syscall.
我希望它可以让你对如何运行其他系统的可执行文件有一个初步的了解, 如果你希望更深入的了解如何模拟syscall, 可以查找rdmsr和wrmsr指令相关的资料.
最后附上我在编写这份示例程序中查阅的链接: