如何实现在的Windows上运行的Linux程序(附示例代码)

2023-11-08

而今天的这篇文章将会讲解如何自己实现一个简单的原生Linux程序运行器, 这个运行器在用户层实现, 原理和Bash On Windows不完全一样,比较接近Linux上的Wine.

示例程序完整的代码在github上, 地址是 https://github.com/303248153/HelloElfLoader

初步了解ELF格式

首先让我们先了解什么是原生Linux程序, 以下说明摘自维基百科

In computing, the Executable and Linkable Format (ELF, formerly named Extensible Linking Format), is a common standard file format for executable files, object code, shared libraries, and core dumps. First published in the specification for the application binary interface (ABI) of the Unix operating system version named System V Release 4 (SVR4),[2] and later in the Tool Interface Standard,[1] it was quickly accepted among different vendors of Unix systems. In 1999, it was chosen as the standard binary file format for Unix and Unix-like systems on x86 processors by the 86open project.
By design, ELF is flexible, extensible, and cross-platform, not bound to any given central processing unit (CPU) or instruction set architecture. This has allowed it to be adopted by many different operating systems on many different hardware platforms.

Linux的可执行文件格式采用了ELF格式, 而Windows采用了PE格式, 也就是我们经常使用的exe文件的格式.

ELF格式的结构如下

如何实现在Windows上运行Linux程序,附示例代码

 

大致上可以分为这些部分

  • ELF头,在文件的最开头,储存了类型和版本等信息
  • 程序头, 供程序运行时解释器(interpreter)使用
  • 节头, 供程序编译时链接器(linker)使用, 运行时不需要读节头
  • 节内容, 不同的节作用都不一样
  • .text 代码节,保存了主要的程序代码
  • .rodata 保存了只读的数据,例如字符串(const char*)
  • .data 保存了可读写的数据,例如全局变量
  • 还有其他各种各样的节

让我们来实际看一下Linux可执行程序的样子

以下的编译环境是Ubuntu 16.04 x64 + gcc 5.4.0, 编译环境不一样可能会得出不同的结果

首先创建hello.c,写入以下的代码

#include <stdio.h>
int max(int x, int y) {
 return x > y ? x : y;
}
int main() {
 printf("max is %d
", max(123, 321));
 printf("test many arguments %d %d %d %s %s %s %s %s %s
", 1, 2, 3, "a", "b", "c", "d", "e", "f");
 return 100;
}

然后使用gcc编译这份代码

gcc hello.c

编译完成后你可以看到hello.c旁边多了一个a.out, 这就是linux的可执行文件了, 现在可以在linux上运行它

./a.out

你可以看到以下输出

max is 321
test many arguments 1 2 3 a b c d e f

我们来看看a.out包含了什么,解析ELF文件可以使用readelf命令

readelf -a ./a.out

可以看到输出了以下的信息

ELF 头:
 Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
 类别: ELF64
 数据: 2 补码,小端序 (little endian)
 版本: 1 (current)
 OS/ABI: UNIX - System V
 ABI 版本: 0
 类型: EXEC (可执行文件)
 系统架构: Advanced Micro Devices X86-64
 版本: 0x1
 入口点地址: 0x400430
 程序头起点: 64 (bytes into file)
 Start of section headers: 6648 (bytes into file)
 标志: 0x0
 本头的大小: 64 (字节)
 程序头大小: 56 (字节)
 Number of program headers: 9
 节头大小: 64 (字节)
 节头数量: 31
 字符串表索引节头: 28
节头:
 [号] 名称 类型 地址 偏移量
 大小 全体大小 旗标 链接 信息 对齐
 [ 0] NULL 0000000000000000 00000000
 0000000000000000 0000000000000000 0 0 0
 [ 1] .interp PROGBITS 0000000000400238 00000238
 000000000000001c 0000000000000000 A 0 0 1
 [ 2] .note.ABI-tag NOTE 0000000000400254 00000254
 0000000000000020 0000000000000000 A 0 0 4
 [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
 0000000000000024 0000000000000000 A 0 0 4
 [ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
 000000000000001c 0000000000000000 A 5 0 8
 [ 5] .dynsym DYNSYM 00000000004002b8 000002b8
 0000000000000060 0000000000000018 A 6 1 8
 [ 6] .dynstr STRTAB 0000000000400318 00000318
 000000000000003f 0000000000000000 A 0 0 1
 [ 7] .gnu.version VERSYM 0000000000400358 00000358
 0000000000000008 0000000000000002 A 5 0 2
 [ 8] .gnu.version_r VERNEED 0000000000400360 00000360
 0000000000000020 0000000000000000 A 6 1 8
 [ 9] .rela.dyn RELA 0000000000400380 00000380
 0000000000000018 0000000000000018 A 5 0 8
 [10] .rela.plt RELA 0000000000400398 00000398
 0000000000000030 0000000000000018 AI 5 24 8
 [11] .init PROGBITS 00000000004003c8 000003c8
 000000000000001a 0000000000000000 AX 0 0 4
 [12] .plt PROGBITS 00000000004003f0 000003f0
 0000000000000030 0000000000000010 AX 0 0 16
 [13] .plt.got PROGBITS 0000000000400420 00000420
 0000000000000008 0000000000000000 AX 0 0 8
 [14] .text PROGBITS 0000000000400430 00000430
 00000000000001f2 0000000000000000 AX 0 0 16
 [15] .fini PROGBITS 0000000000400624 00000624
 0000000000000009 0000000000000000 AX 0 0 4
 [16] .rodata PROGBITS 0000000000400630 00000630
 0000000000000050 0000000000000000 A 0 0 8
 [17] .eh_frame_hdr PROGBITS 0000000000400680 00000680
 000000000000003c 0000000000000000 A 0 0 4
 [18] .eh_frame PROGBITS 00000000004006c0 000006c0
 0000000000000114 0000000000000000 A 0 0 8
 [19] .init_array INIT_ARRAY 0000000000600e10 00000e10
 0000000000000008 0000000000000000 WA 0 0 8
 [20] .fini_array FINI_ARRAY 0000000000600e18 00000e18
 0000000000000008 0000000000000000 WA 0 0 8
 [21] .jcr PROGBITS 0000000000600e20 00000e20
 0000000000000008 0000000000000000 WA 0 0 8
 [22] .dynamic DYNAMIC 0000000000600e28 00000e28
 00000000000001d0 0000000000000010 WA 6 0 8
 [23] .got PROGBITS 0000000000600ff8 00000ff8
 0000000000000008 0000000000000008 WA 0 0 8
 [24] .got.plt PROGBITS 0000000000601000 00001000
 0000000000000028 0000000000000008 WA 0 0 8
 [25] .data PROGBITS 0000000000601028 00001028
 0000000000000010 0000000000000000 WA 0 0 8
 [26] .bss NOBITS 0000000000601038 00001038
 0000000000000008 0000000000000000 WA 0 0 1
 [27] .comment PROGBITS 0000000000000000 00001038
 0000000000000034 0000000000000001 MS 0 0 1
 [28] .shstrtab STRTAB 0000000000000000 000018ea
 000000000000010c 0000000000000000 0 0 1
 [29] .symtab SYMTAB 0000000000000000 00001070
 0000000000000660 0000000000000018 30 47 8
 [30] .strtab STRTAB 0000000000000000 000016d0
 000000000000021a 0000000000000000 0 0 1
Key to Flags:
 W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
 I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
 O (extra OS processing required) o (OS specific), p (processor specific)
There are no section groups in this file.
程序头:
 Type Offset VirtAddr PhysAddr
 FileSiz MemSiz Flags Align
 PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
 0x00000000000001f8 0x00000000000001f8 R E 8
 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
 0x000000000000001c 0x000000000000001c R 1
 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
 LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
 0x00000000000007d4 0x00000000000007d4 R E 200000
 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
 0x0000000000000228 0x0000000000000230 RW 200000
 DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
 0x00000000000001d0 0x00000000000001d0 RW 8
 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
 0x0000000000000044 0x0000000000000044 R 4
 GNU_EH_FRAME 0x0000000000000680 0x0000000000400680 0x0000000000400680
 0x000000000000003c 0x000000000000003c R 4
 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
 0x0000000000000000 0x0000000000000000 RW 10
 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
 0x00000000000001f0 0x00000000000001f0 R 1
 Section to Segment mapping:
 段节...
 00 
 01 .interp 
 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
 04 .dynamic 
 05 .note.ABI-tag .note.gnu.build-id 
 06 .eh_frame_hdr 
 07 
 08 .init_array .fini_array .jcr .dynamic .got 
Dynamic section at offset 0xe28 contains 24 entries:
 标记 类型 名称/值
 0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
 0x000000000000000c (INIT) 0x4003c8
 0x000000000000000d (FINI) 0x400624
 0x0000000000000019 (INIT_ARRAY) 0x600e10
 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
 0x000000000000001a (FINI_ARRAY) 0x600e18
 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
 0x000000006ffffef5 (GNU_HASH) 0x400298
 0x0000000000000005 (STRTAB) 0x400318
 0x0000000000000006 (SYMTAB) 0x4002b8
 0x000000000000000a (STRSZ) 63 (bytes)
 0x000000000000000b (SYMENT) 24 (bytes)
 0x0000000000000015 (DEBUG) 0x0
 0x0000000000000003 (PLTGOT) 0x601000
 0x0000000000000002 (PLTRELSZ) 48 (bytes)
 0x0000000000000014 (PLTREL) RELA
 0x0000000000000017 (JMPREL) 0x400398
 0x0000000000000007 (RELA) 0x400380
 0x0000000000000008 (RELASZ) 24 (bytes)
 0x0000000000000009 (RELAENT) 24 (bytes)
 0x000000006ffffffe (VERNEED) 0x400360
 0x000000006fffffff (VERNEEDNUM) 1
 0x000000006ffffff0 (VERSYM) 0x400358
 0x0000000000000000 (NULL) 0x0
重定位节 '.rela.dyn' 位于偏移量 0x380 含有 1 个条目:
 偏移量 信息 类型 符号值 符号名称 + 加数
000000600ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
 偏移量 信息 类型 符号值 符号名称 + 加数
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.
Symbol table '.dynsym' contains 4 entries:
 Num: Value Size Type Bind Vis Ndx Name
 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 
 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
Symbol table '.symtab' contains 68 entries:
 Num: Value Size Type Bind Vis Ndx Name
 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 
 1: 0000000000400238 0 SECTION LOCAL DEFAULT 1 
 2: 0000000000400254 0 SECTION LOCAL DEFAULT 2 
 3: 0000000000400274 0 SECTION LOCAL DEFAULT 3 
 4: 0000000000400298 0 SECTION LOCAL DEFAULT 4 
 5: 00000000004002b8 0 SECTION LOCAL DEFAULT 5 
 6: 0000000000400318 0 SECTION LOCAL DEFAULT 6 
 7: 0000000000400358 0 SECTION LOCAL DEFAULT 7 
 8: 0000000000400360 0 SECTION LOCAL DEFAULT 8 
 9: 0000000000400380 0 SECTION LOCAL DEFAULT 9 
 10: 0000000000400398 0 SECTION LOCAL DEFAULT 10 
 11: 00000000004003c8 0 SECTION LOCAL DEFAULT 11 
 12: 00000000004003f0 0 SECTION LOCAL DEFAULT 12 
 13: 0000000000400420 0 SECTION LOCAL DEFAULT 13 
 14: 0000000000400430 0 SECTION LOCAL DEFAULT 14 
 15: 0000000000400624 0 SECTION LOCAL DEFAULT 15 
 16: 0000000000400630 0 SECTION LOCAL DEFAULT 16 
 17: 0000000000400680 0 SECTION LOCAL DEFAULT 17 
 18: 00000000004006c0 0 SECTION LOCAL DEFAULT 18 
 19: 0000000000600e10 0 SECTION LOCAL DEFAULT 19 
 20: 0000000000600e18 0 SECTION LOCAL DEFAULT 20 
 21: 0000000000600e20 0 SECTION LOCAL DEFAULT 21 
 22: 0000000000600e28 0 SECTION LOCAL DEFAULT 22 
 23: 0000000000600ff8 0 SECTION LOCAL DEFAULT 23 
 24: 0000000000601000 0 SECTION LOCAL DEFAULT 24 
 25: 0000000000601028 0 SECTION LOCAL DEFAULT 25 
 26: 0000000000601038 0 SECTION LOCAL DEFAULT 26 
 27: 0000000000000000 0 SECTION LOCAL DEFAULT 27 
 28: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
 29: 0000000000600e20 0 OBJECT LOCAL DEFAULT 21 __JCR_LIST__
 30: 0000000000400460 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
 31: 00000000004004a0 0 FUNC LOCAL DEFAULT 14 register_tm_clones
 32: 00000000004004e0 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
 33: 0000000000601038 1 OBJECT LOCAL DEFAULT 26 completed.7585
 34: 0000000000600e18 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fin
 35: 0000000000400500 0 FUNC LOCAL DEFAULT 14 frame_dummy
 36: 0000000000600e10 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_
 37: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
 38: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
 39: 00000000004007d0 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
 40: 0000000000600e20 0 OBJECT LOCAL DEFAULT 21 __JCR_END__
 41: 0000000000000000 0 FILE LOCAL DEFAULT ABS 
 42: 0000000000600e18 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
 43: 0000000000600e28 0 OBJECT LOCAL DEFAULT 22 _DYNAMIC
 44: 0000000000600e10 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
 45: 0000000000400680 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
 46: 0000000000601000 0 OBJECT LOCAL DEFAULT 24 _GLOBAL_OFFSET_TABLE_
 47: 0000000000400620 2 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
 48: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
 49: 0000000000601028 0 NOTYPE WEAK DEFAULT 25 data_start
 50: 0000000000601038 0 NOTYPE GLOBAL DEFAULT 25 _edata
 51: 0000000000400624 0 FUNC GLOBAL DEFAULT 15 _fini
 52: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
 53: 0000000000400526 22 FUNC GLOBAL DEFAULT 14 max
 54: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
 55: 0000000000601028 0 NOTYPE GLOBAL DEFAULT 25 __data_start
 56: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
 57: 0000000000601030 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
 58: 0000000000400630 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
 59: 00000000004005b0 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
 60: 0000000000601040 0 NOTYPE GLOBAL DEFAULT 26 _end
 61: 0000000000400430 42 FUNC GLOBAL DEFAULT 14 _start
 62: 0000000000601038 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
 63: 000000000040053c 109 FUNC GLOBAL DEFAULT 14 main
 64: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
 65: 0000000000601038 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
 66: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
 67: 00000000004003c8 0 FUNC GLOBAL DEFAULT 11 _init
Version symbols section '.gnu.version' contains 4 entries:
 地址: 0000000000400358 Offset: 0x000358 Link: 5 (.dynsym)
 000: 0 (*本地*) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 0 (*本地*) 
Version needs section '.gnu.version_r' contains 1 entries:
 地址:0x0000000000400360 Offset: 0x000360 Link: 6 (.dynstr)
 000000: 版本: 1 文件:libc.so.6 计数:1
 0x0010:名称:GLIBC_2.2.5 标志:无 版本:2
Displaying notes found at file offset 0x00000254 with length 0x00000020:
 Owner Data size Description
 GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
 OS: Linux, ABI: 2.6.32
Displaying notes found at file offset 0x00000274 with length 0x00000024:
 Owner Data size Description
 GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
 Build ID: debd3d7912be860a432b5c685a6cff7fd9418528

从上面的信息中我们可以知道这个文件的类型是ELF64, 也就是64位的可执行程序, 并且有9个程序头和31个节头, 各个节的作用大家可以在网上找到资料, 这篇文章中只涉及到以下的节

  • .init 程序初始化的代码
  • .rela.dyn 需要重定位的变量列表
  • .rela.plt 需要重定位的函数列表
  • .plt 调用动态链接函数的代码
  • .text 保存了主要的程序代码
  • .init 保存了程序的初始化代码, 用于初始化全局变量等
  • .fini 保存了程序的终止代码, 用于析构全局变量等
  • .rodata 保存了只读的数据,例如字符串(const char*)
  • .data 保存了可读写的数据,例如全局变量
  • .dynsym 动态链接的符号表
  • .dynstr 动态链接的符号名称字符串
  • .dynamic 动态链接所需要的信息,供程序运行时使用(不需要访问节头)

什么是动态链接

上面的程序中调用了printf函数, 然而这个函数的实现并不在./a.out中, 那么printf函数在哪里, 又是怎么被调用的?

printf函数的实现在glibc库中, 也就是/lib/x86_64-linux-gnu/libc.so.6中, 在执行./a.out的时候会在glibc库中找到这个函数并进行调用, 我们来看看这段代码

执行以下命令反编译./a.out

objdump -c -S ./a.out

我们可以看到以下的代码

00000000004003f0 <printf@plt-0x10>:
 4003f0: ff 35 12 0c 20 00 pushq 0x200c12(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
 4003f6: ff 25 14 0c 20 00 jmpq *0x200c14(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
 4003fc: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400400 <printf@plt>:
 400400: ff 25 12 0c 20 00 jmpq *0x200c12(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
 400406: 68 00 00 00 00 pushq $0x0
 40040b: e9 e0 ff ff ff jmpq 4003f0 <_init+0x28>
000000000040053c <main>:
 40053c: 55 push %rbp
 40053d: 48 89 e5 mov %rsp,%rbp
 400540: be 41 01 00 00 mov $0x141,%esi
 400545: bf 7b 00 00 00 mov $0x7b,%edi
 40054a: e8 d7 ff ff ff callq 400526 <max>
 40054f: 89 c6 mov %eax,%esi
 400551: bf 38 06 40 00 mov $0x400638,%edi
 400556: b8 00 00 00 00 mov $0x0,%eax
 40055b: e8 a0 fe ff ff callq 400400 <printf@plt>

在这一段代码中,我们可以看到调用printf会首先调用0x400400的printf@plt

printf@plt会负责在运行时找到实际的printf函数并跳转到该函数

在这里实际的printf函数会保存在0x400406 + 0x200c12 = 0x601018中

需要注意的是0x601018一开始并不会指向实际的printf函数,而是会指向0x400406, 为什么会这样? 因为Linux的可执行程序为了考虑性能,不会在一开始就解决所有动态连接的函数,而是选择了延迟解决.

在上面第一次jmpq *0x200c12(%rip)会跳转到下一条指令0x400406, 又会继续跳转到0x4003f0, 再跳转到0x601010指向的地址, 0x601010指向的地址就是延迟解决的实现, 第一次延迟解决成功后, 0x601018就会指向实际的printf, 以后调用就会直接跳转到实际的printf上.

程序入口点

Linux程序运行首先会从_start函数开始, 上面readelf中的入口点地址0x400430就是_start函数的地址,

0000000000400430 <_start>:
 400430: 31 ed xor %ebp,%ebp
 400432: 49 89 d1 mov %rdx,%r9
 400435: 5e pop %rsi
 400436: 48 89 e2 mov %rsp,%rdx
 400439: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
 40043d: 50 push %rax
 40043e: 54 push %rsp
 40043f: 49 c7 c0 20 06 40 00 mov $0x400620,%r8
 400446: 48 c7 c1 b0 05 40 00 mov $0x4005b0,%rcx
 40044d: 48 c7 c7 3c 05 40 00 mov $0x40053c,%rdi
 400454: e8 b7 ff ff ff callq 400410 <__libc_start_main@plt>
 400459: f4 hlt 
 40045a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)

接下来_start函数会调用__libc_start_main函数, __libc_start_main是libc库中定义的初始化函数, 负责初始化全局变量和调用main函数等工作.

__libc_start_main函数还负责设置返回值和退出进程, 可以看到上面调用__libc_start_main后的指令是hlt, 这个指令永远不会被执行.

实现Linux程序运行器

在拥有以上的知识后我们可以先构想以下的运行器需要做什么.

因为x64的Windows和Linux程序使用的cpu指令集都是一样的,我们可以直接执行汇编而不需要一个指令模拟器,

而且这次我打算在用户层实现, 所以不能像Bash On Windows一样模拟syscall, 这个运行器会像下图一样模拟libc库的函数

如何实现在Windows上运行Linux程序,附示例代码

 

这样运行器需要做的事情有:

  • 解析ELF文件
  • 加载程序代码到指定的内存地址
  • 加载数据到指定的内存地址
  • 提供动态链接的函数实现
  • 执行加载的程序代码

这些工作会在以下的示例程序中一一实现, 完整的源代码可以看文章顶部的链接

首先我们需要把ELF文件格式对应的代码从binutils中复制过来, 它包含了ELF头, 程序头和相关的数据结构, 里面用unsigned char[]是为了防止alignment, 这样结构体可以直接从文件内容中转换过来

ELFDefine.h:

#pragma once
namespace HelloElfLoader {
 // 以下内容复制自
 // https://github.com/aeste/binutils/blob/develop/elfcpp/elfcpp.h
 // https://github.com/aeste/binutils/blob/develop/include/elf/external.h
 // e_ident中各项的偏移值
 const int EI_MAG0 = 0;
 const int EI_MAG1 = 1;
 const int EI_MAG2 = 2;
 const int EI_MAG3 = 3;
 const int EI_CLASS = 4;
 const int EI_DATA = 5;
 const int EI_VERSION = 6;
 const int EI_OSABI = 7;
 const int EI_ABIVERSION = 8;
 const int EI_PAD = 9;
 const int EI_NIDENT = 16;
 // ELF文件类型
 enum {
 ELFCLASSNONE = 0,
 ELFCLASS32 = 1,
 ELFCLASS64 = 2
 };
 // ByteOrder
 enum {
 ELFDATANONE = 0,
 ELFDATA2LSB = 1,
 ELFDATA2MSB = 2
 };
 // 程序头类型
 enum PT
 {
 PT_NULL = 0,
 PT_LOAD = 1,
 PT_DYNAMIC = 2,
 PT_INTERP = 3,
 PT_NOTE = 4,
 PT_SHLIB = 5,
 PT_PHDR = 6,
 PT_TLS = 7,
 PT_LOOS = 0x60000000,
 PT_HIOS = 0x6fffffff,
 PT_LOPROC = 0x70000000,
 PT_HIPROC = 0x7fffffff,
 // The remaining values are not in the standard.
 // Frame unwind information.
 PT_GNU_EH_FRAME = 0x6474e550,
 PT_SUNW_EH_FRAME = 0x6474e550,
 // Stack flags.
 PT_GNU_STACK = 0x6474e551,
 // Read only after relocation.
 PT_GNU_RELRO = 0x6474e552,
 // Platform architecture compatibility information
 PT_ARM_ARCHEXT = 0x70000000,
 // Exception unwind tables
 PT_ARM_EXIDX = 0x70000001
 };
 // 动态节类型
 enum DT
 {
 DT_NULL = 0,
 DT_NEEDED = 1,
 DT_PLTRELSZ = 2,
 DT_PLTGOT = 3,
 DT_HASH = 4,
 DT_STRTAB = 5,
 DT_SYMTAB = 6,
 DT_RELA = 7,
 DT_RELASZ = 8,
 DT_RELAENT = 9,
 DT_STRSZ = 10,
 DT_SYMENT = 11,
 DT_INIT = 12,
 DT_FINI = 13,
 DT_SONAME = 14,
 DT_RPATH = 15,
 DT_SYMBOLIC = 16,
 DT_REL = 17,
 DT_RELSZ = 18,
 DT_RELENT = 19,
 DT_PLTREL = 20,
 DT_DEBUG = 21,
 DT_TEXTREL = 22,
 DT_JMPREL = 23,
 DT_BIND_NOW = 24,
 DT_INIT_ARRAY = 25,
 DT_FINI_ARRAY = 26,
 DT_INIT_ARRAYSZ = 27,
 DT_FINI_ARRAYSZ = 28,
 DT_RUNPATH = 29,
 DT_FLAGS = 30,
 // This is used to mark a range of dynamic tags. It is not really
 // a tag value.
 DT_ENCODING = 32,
 DT_PREINIT_ARRAY = 32,
 DT_PREINIT_ARRAYSZ = 33,
 DT_LOOS = 0x6000000d,
 DT_HIOS = 0x6ffff000,
 DT_LOPROC = 0x70000000,
 DT_HIPROC = 0x7fffffff,
 // The remaining values are extensions used by GNU or Solaris.
 DT_VALRNGLO = 0x6ffffd00,
 DT_GNU_PRELINKED = 0x6ffffdf5,
 DT_GNU_CONFLICTSZ = 0x6ffffdf6,
 DT_GNU_LIBLISTSZ = 0x6ffffdf7,
 DT_CHECKSUM = 0x6ffffdf8,
 DT_PLTPADSZ = 0x6ffffdf9,
 DT_MOVEENT = 0x6ffffdfa,
 DT_MOVESZ = 0x6ffffdfb,
 DT_FEATURE = 0x6ffffdfc,
 DT_POSFLAG_1 = 0x6ffffdfd,
 DT_SYMINSZ = 0x6ffffdfe,
 DT_SYMINENT = 0x6ffffdff,
 DT_VALRNGHI = 0x6ffffdff,
 DT_ADDRRNGLO = 0x6ffffe00,
 DT_GNU_HASH = 0x6ffffef5,
 DT_TLSDESC_PLT = 0x6ffffef6,
 DT_TLSDESC_GOT = 0x6ffffef7,
 DT_GNU_CONFLICT = 0x6ffffef8,
 DT_GNU_LIBLIST = 0x6ffffef9,
 DT_CONFIG = 0x6ffffefa,
 DT_DEPAUDIT = 0x6ffffefb,
 DT_AUDIT = 0x6ffffefc,
 DT_PLTPAD = 0x6ffffefd,
 DT_MOVETAB = 0x6ffffefe,
 DT_SYMINFO = 0x6ffffeff,
 DT_ADDRRNGHI = 0x6ffffeff,
 DT_RELACOUNT = 0x6ffffff9,
 DT_RELCOUNT = 0x6ffffffa,
 DT_FLAGS_1 = 0x6ffffffb,
 DT_VERDEF = 0x6ffffffc,
 DT_VERDEFNUM = 0x6ffffffd,
 DT_VERNEED = 0x6ffffffe,
 DT_VERNEEDNUM = 0x6fffffff,
 DT_VERSYM = 0x6ffffff0,
 // Specify the value of _GLOBAL_OFFSET_TABLE_.
 DT_PPC_GOT = 0x70000000,
 // Specify the start of the .glink section.
 DT_PPC64_GLINK = 0x70000000,
 // Specify the start and size of the .opd section.
 DT_PPC64_OPD = 0x70000001,
 DT_PPC64_OPDSZ = 0x70000002,
 // The index of an STT_SPARC_REGISTER symbol within the DT_SYMTAB
 // symbol table. One dynamic entry exists for every STT_SPARC_REGISTER
 // symbol in the symbol table.
 DT_SPARC_REGISTER = 0x70000001,
 DT_AUXILIARY = 0x7ffffffd,
 DT_USED = 0x7ffffffe,
 DT_FILTER = 0x7fffffff
 };;
 // ELF头的定义
 typedef struct {
 unsigned char e_ident[16]; /* ELF "magic number" */
 unsigned char e_type[2]; /* Identifies object file type */
 unsigned char e_machine[2]; /* Specifies required architecture */
 unsigned char e_version[4]; /* Identifies object file version */
 unsigned char e_entry[8]; /* Entry point virtual address */
 unsigned char e_phoff[8]; /* Program header table file offset */
 unsigned char e_shoff[8]; /* Section header table file offset */
 unsigned char e_flags[4]; /* Processor-specific flags */
 unsigned char e_ehsize[2]; /* ELF header size in bytes */
 unsigned char e_phentsize[2]; /* Program header table entry size */
 unsigned char e_phnum[2]; /* Program header table entry count */
 unsigned char e_shentsize[2]; /* Section header table entry size */
 unsigned char e_shnum[2]; /* Section header table entry count */
 unsigned char e_shstrndx[2]; /* Section header string table index */
 } Elf64_External_Ehdr;
 // 程序头的定义
 typedef struct {
 unsigned char p_type[4]; /* Identifies program segment type */
 unsigned char p_flags[4]; /* Segment flags */
 unsigned char p_offset[8]; /* Segment file offset */
 unsigned char p_vaddr[8]; /* Segment virtual address */
 unsigned char p_paddr[8]; /* Segment physical address */
 unsigned char p_filesz[8]; /* Segment size in file */
 unsigned char p_memsz[8]; /* Segment size in memory */
 unsigned char p_align[8]; /* Segment alignment, file & memory */
 } Elf64_External_Phdr;
 // DYNAMIC类型的程序头的内容定义
 typedef struct {
 unsigned char d_tag[8]; /* entry tag value */
 union {
 unsigned char d_val[8];
 unsigned char d_ptr[8];
 } d_un;
 } Elf64_External_Dyn;
 // 动态链接的重定位记录,部分系统会用Elf64_External_Rel
 typedef struct {
 unsigned char r_offset[8]; /* Location at which to apply the action */
 unsigned char r_info[8]; /* index and type of relocation */
 unsigned char r_addend[8]; /* Constant addend used to compute value */
 } Elf64_External_Rela;
 // 动态链接的符号信息
 typedef struct {
 unsigned char st_name[4]; /* Symbol name, index in string tbl */
 unsigned char st_info[1]; /* Type and binding attributes */
 unsigned char st_other[1]; /* No defined meaning, 0 */
 unsigned char st_shndx[2]; /* Associated section index */
 unsigned char st_value[8]; /* Value of the symbol */
 unsigned char st_size[8]; /* Associated symbol size */
 } Elf64_External_Sym;
}

接下来我们定义一个读取和执行ELF文件的类, 这个类会在初始化时把文件加载到fileStream_, execute函数会负责执行

HelloElfLoader.h:

#pragma once
#include <string>
#include <fstream>
namespace HelloElfLoader {
 class Loader {
 std::ifstream fileStream_;
 public:
 Loader(const std::string& path);
 Loader(std::ifstream&& fileStream);
 void execute();
 };
}

构造函数如下, 也就是标准的c++打开文件的代码

HelloElfLoader.cpp:

Loader::Loader(const std::string& path) :
 Loader(std::ifstream(path, std::ios::in | std::ios::binary)) {}
Loader::Loader(std::ifstream&& fileStream) :
 fileStream_(std::move(fileStream)) {
 if (!fileStream_) {
 throw std::runtime_error("open file failed");
 }
}

接下来将实现上面所说的步骤, 首先是解析ELF文件

void Loader::execute() {
 std::cout << "====== start loading elf ======" << std::endl;
 // 检查当前运行程序是否64位
 if (sizeof(intptr_t) != sizeof(std::int64_t)) {
 throw std::runtime_error("please use x64 compile and run this program");
 }
 // 读取ELF头
 Elf64_External_Ehdr elfHeader = {};
 fileStream_.seekg(0);
 fileStream_.read(reinterpret_cast<char*>(&elfHeader), sizeof(elfHeader));
 // 检查ELF头,只支持64位且byte order是little endian的程序
 if (std::string(reinterpret_cast<char*>(elfHeader.e_ident), 4) != "\x7f\x45\x4c\x46") {
 throw std::runtime_error("magic not match");
 }
 else if (elfHeader.e_ident[EI_CLASS] != ELFCLASS64) {
 throw std::runtime_error("only support ELF64");
 }
 else if (elfHeader.e_ident[EI_DATA] != ELFDATA2LSB) {
 throw std::runtime_error("only support little endian");
 }
 // 获取program table的信息
 std::uint32_t programTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_phoff);
 std::uint16_t programTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phentsize);
 std::uint16_t programTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phnum);
 std::cout << "program table at: " << programTableOffset << ", "
 << programTableEntryNum << " x " << programTableEntrySize << std::endl;
 // 获取section table的信息
 // section table只给linker用,loader中其实不需要访问section table
 std::uint32_t sectionTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_shoff);
 std::uint16_t sectionTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
 std::uint16_t sectionTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
 std::cout << "section table at: " << sectionTableOffset << ", "
 << sectionTableEntryNum << " x " << sectionTableEntrySize << std::endl;

ELF文件的的开始部分就是ELF头,和Elf64_External_Ehdr结构体的结构相同, 我们可以读到Elf64_External_Ehdr结构体中,

然后ELF头包含了程序头和节头的偏移值, 我们可以预先获取到这些参数

节头在运行时不需要使用, 运行时需要遍历程序头

 // 准备动态链接的信息
 std::uint64_t jmpRelAddr = 0; // 重定位记录的开始地址
 std::uint64_t pltRelType = 0; // 重定位记录的类型 RELA或REL
 std::uint64_t pltRelSize = 0; // 重定位记录的总大小
 std::uint64_t symTabAddr = 0; // 动态符号表的开始地址
 std::uint64_t strTabAddr = 0; // 动态符号名称表的开始地址
 std::uint64_t strTabSize = 0; // 动态符号名称表的总大小
 // 遍历program hedaer
 std::vector<Elf64_External_Phdr> programHeaders;
 programHeaders.resize(programTableEntryNum);
 fileStream_.read(reinterpret_cast<char*>(programHeaders.data()), programTableEntryNum * programTableEntrySize);
 std::vector<std::shared_ptr<void>> loadedSegments;
 for (const auto& programHeader : programHeaders) {
 std::uint32_t type = *reinterpret_cast<const std::uint32_t*>(programHeader.p_type);
 if (type == PT_LOAD) {
 // 把文件内容(包含程序代码和数据)加载到虚拟内存,这个示例不考虑地址冲突
 std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
 std::uint64_t fileSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_filesz);
 std::uint64_t virtAddr = *reinterpret_cast<const std::uint64_t*>(programHeader.p_vaddr);
 std::uint64_t memSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_memsz);
 if (memSize < fileSize) {
 throw std::runtime_error("invalid memsz in program header, it shouldn't less than filesz");
 }
 // 在指定的虚拟地址分配内存
 std::cout << std::hex << "allocate address at: 0x" << virtAddr <<
 " size: 0x" << memSize << std::dec << std::endl;
 void* addr = ::VirtualAlloc((void*)virtAddr, memSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
 if (addr == nullptr) {
 throw std::runtime_error("allocate memory at specific address failed");
 }
 loadedSegments.emplace_back(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
 // 复制文件内容到虚拟内存
 fileStream_.seekg(fileOffset);
 if (!fileStream_.read(reinterpret_cast<char*>(addr), fileSize)) {
 throw std::runtime_error("read contents into memory from LOAD program header failed");
 }
 }
 else if (type == PT_DYNAMIC) {
 // 遍历动态节
 std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
 fileStream_.seekg(fileOffset);
 Elf64_External_Dyn dynSection = {};
 std::uint64_t dynSectionTag = 0;
 std::uint64_t dynSectionVal = 0;
 do {
 if (!fileStream_.read(reinterpret_cast<char*>(&dynSection), sizeof(dynSection))) {
 throw std::runtime_error("read dynamic section failed");
 }
 dynSectionTag = *reinterpret_cast<const std::uint64_t*>(dynSection.d_tag);
 dynSectionVal = *reinterpret_cast<const std::uint64_t*>(dynSection.d_un.d_val);
 if (dynSectionTag == DT_JMPREL) {
 jmpRelAddr = dynSectionVal;
 }
 else if (dynSectionTag == DT_PLTREL) {
 pltRelType = dynSectionVal;
 }
 else if (dynSectionTag == DT_PLTRELSZ) {
 pltRelSize = dynSectionVal;
 }
 else if (dynSectionTag == DT_SYMTAB) {
 symTabAddr = dynSectionVal;
 }
 else if (dynSectionTag == DT_STRTAB) {
 strTabAddr = dynSectionVal;
 }
 else if (dynSectionTag == DT_STRSZ) {
 strTabSize = dynSectionVal;
 }
 } while (dynSectionTag != 0);
 }
 }

还记得我们上面使用readelf读取到的信息吗?

程序头:
 Type Offset VirtAddr PhysAddr
 FileSiz MemSiz Flags Align
 PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
 0x00000000000001f8 0x00000000000001f8 R E 8
 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
 0x000000000000001c 0x000000000000001c R 1
 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
 LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
 0x00000000000007d4 0x00000000000007d4 R E 200000
 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
 0x0000000000000228 0x0000000000000230 RW 200000
 DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
 0x00000000000001d0 0x00000000000001d0 RW 8
 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
 0x0000000000000044 0x0000000000000044 R 4
 GNU_EH_FRAME 0x0000000000000680 0x0000000000400680 0x0000000000400680
 0x000000000000003c 0x000000000000003c R 4
 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
 0x0000000000000000 0x0000000000000000 RW 10
 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
 0x00000000000001f0 0x00000000000001f0 R 1

这里面类型是LOAD的头代表需要加载文件的内容到内存,

Offset是文件的偏移值, VirtAddr是虚拟内存地址, FileSiz是需要加载的文件大小, MemSiz是需要分配的内存大小, Flags是内存的访问权限,

这个示例不考虑访问权限(统一使用PAGE_EXECUTE_READWRITE).

这个程序有两个LOAD头, 第一个包含了代码和只读数据(.data, .init, .rodata等节的内容), 第二个包含了可写数据(.init_array, .fini_array等节的内容).

把LOAD头对应的内容加载到指定的内存地址后我们就完成了构想中的第2个第3个步骤, 现在代码和数据都在内存中了.

接下来我们还需要处理动态链接的函数, 处理所需的信息可以从DYNAMIC头得到

DYNAMIC头包含的信息有

Dynamic section at offset 0xe28 contains 24 entries:
 标记 类型 名称/值
 0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
 0x000000000000000c (INIT) 0x4003c8
 0x000000000000000d (FINI) 0x400624
 0x0000000000000019 (INIT_ARRAY) 0x600e10
 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
 0x000000000000001a (FINI_ARRAY) 0x600e18
 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
 0x000000006ffffef5 (GNU_HASH) 0x400298
 0x0000000000000005 (STRTAB) 0x400318
 0x0000000000000006 (SYMTAB) 0x4002b8
 0x000000000000000a (STRSZ) 63 (bytes)
 0x000000000000000b (SYMENT) 24 (bytes)
 0x0000000000000015 (DEBUG) 0x0
 0x0000000000000003 (PLTGOT) 0x601000
 0x0000000000000002 (PLTRELSZ) 48 (bytes)
 0x0000000000000014 (PLTREL) RELA
 0x0000000000000017 (JMPREL) 0x400398
 0x0000000000000007 (RELA) 0x400380
 0x0000000000000008 (RELASZ) 24 (bytes)
 0x0000000000000009 (RELAENT) 24 (bytes)
 0x000000006ffffffe (VERNEED) 0x400360
 0x000000006fffffff (VERNEEDNUM) 1
 0x000000006ffffff0 (VERSYM) 0x400358
 0x0000000000000000 (NULL) 0x0

一个个看上面代码中涉及到的类型

  • DT_JMPREL: 重定位记录的开始地址, 指向.rela.plt节在内存中保存的地址
  • DT_PLTREL: 重定位记录的类型 RELA或RE, 这里是RELAL
  • DT_PLTRELSZ: 重定位记录的总大小, 这里是24 * 2 = 48
重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
 偏移量 信息 类型 符号值 符号名称 + 加数
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
  • DT_SYMTAB: 动态符号表的开始地址, 指向.dynsym节在内存中保存的地址
  • DT_STRTAB: 动态符号名称表的开始地址, 指向.dynstr节在内存中保存的地址
  • DT_STRSZ: 动态符号名称表的总大小
Symbol table '.dynsym' contains 4 entries:
 Num: Value Size Type Bind Vis Ndx Name
 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 
 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__

在遍历完程序头以后, 我们可以知道有两个动态链接的函数需要重定位, 它们分别是__libc_start_main和printf, 其中__libc_start_main负责调用main函数

接下来让我们需要设置这些函数的地址

 // 读取动态链接符号表
 std::string dynamicSymbolNames(reinterpret_cast<char*>(strTabAddr), strTabSize);
 Elf64_External_Sym* dynamicSymbols = reinterpret_cast<Elf64_External_Sym*>(symTabAddr);
 // 设置动态链接的函数地址
 std::cout << std::hex << "read dynamic entires at: 0x" << jmpRelAddr <<
 " size: 0x" << pltRelSize << std::dec << std::endl;
 if (jmpRelAddr == 0 || pltRelType != DT_RELA || pltRelSize % sizeof(Elf64_External_Rela) != 0) {
 throw std::runtime_error("invalid dynamic entry info, rel type should be rela");
 }
 std::vector<std::shared_ptr<void>> libraryFuncs;
 for (std::uint64_t offset = 0; offset < pltRelSize; offset += sizeof(Elf64_External_Rela)) {
 Elf64_External_Rela* rela = (Elf64_External_Rela*)(jmpRelAddr + offset);
 std::uint64_t relaOffset = *reinterpret_cast<const std::uint64_t*>(rela->r_offset);
 std::uint64_t relaInfo = *reinterpret_cast<const std::uint64_t*>(rela->r_info);
 std::uint64_t relaSym = relaInfo >> 32; // ELF64_R_SYM
 std::uint64_t relaType = relaInfo & 0xffffffff; // ELF64_R_TYPE
 // 获取符号
 Elf64_External_Sym* symbol = dynamicSymbols + relaSym;
 std::uint32_t symbolNameOffset = *reinterpret_cast<std::uint32_t*>(symbol->st_name);
 std::string symbolName(dynamicSymbolNames.data() + symbolNameOffset);
 std::cout << "relocate symbol: " << symbolName << std::endl;
 // 替换函数地址
 // 原本应该延迟解决,这里图简单就直接覆盖了
 void** relaPtr = reinterpret_cast<void**>(relaOffset);
 std::shared_ptr<void> func = resolveLibraryFunc(symbolName);
 if (func == nullptr) {
 throw std::runtime_error("unsupport symbol name");
 }
 libraryFuncs.emplace_back(func);
 *relaPtr = func.get();
 }

上面的代码遍历了DT_JMPREL重定位记录, 并且在加载时设置了这些函数的地址,

其实应该通过延迟解决实现的, 但是这里为了简单就直接替换成最终的地址了.

上面获取函数实际地址的逻辑我写到了resolveLibraryFunc中,这个函数的实现在另外一个文件, 如下

namespace HelloElfLoader {
 namespace {
 // 原始的返回地址
 thread_local void* originalReturnAddress = nullptr;
 void* getOriginalReturnAddress() {
 return originalReturnAddress;
 }
 void setOriginalReturnAddress(void* address) {
 originalReturnAddress = address;
 }
 // 模拟libc调用main的函数,目前不支持传入argc和argv
 void __libc_start_main(int(*main)()) {
 std::cout << "call main: " << main << std::endl;
 int ret = main();
 std::cout << "result: " << ret << std::endl;
 std::exit(0);
 }
 // 模拟printf函数
 int printf(const char* fmt, ...) {
 int ret;
 va_list myargs;
 va_start(myargs, fmt);
 ret = ::vprintf(fmt, myargs);
 va_end(myargs);
 return ret;
 }
 // 把System V AMD64 ABI转换为Microsoft x64 calling convention
 // 因为vc++不支持inline asm,只能直接写hex
 // 这个函数支持任意长度的参数,但是性能会有损耗,如果参数数量已知可以编写更快的loader代码 
 const char generic_func_loader[]{
 // 让参数连续排列在栈上
 // [第一个参数] [第二个参数] [第三个参数] ...
 0x58, // pop %rax 暂存原返回地址
 0x41, 0x51, // push %r9 入栈第六个参数,之后的参数都在后续的栈上
 0x41, 0x50, // push %r8 入栈第五个参数
 0x51, // push %rcx 入栈第四个参数
 0x52, // push %rdx 入栈第三个参数
 0x56, // push %rsi 入栈第二个参数
 0x57, // push %rdi 入栈第一个参数
 // 调用setOriginalReturnAddress保存原返回地址
 0x48, 0x89, 0xc1, // mov %rax, %rcx 第一个参数是原返回地址
 0x48, 0x83, 0xec, 0x20, // sub $0x20, %rsp 预留32位的影子空间
 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
 0xff, 0xd0, // callq *%rax 调用setOriginalReturnAddress
 0x48, 0x83, 0xc4, 0x20, // add %0x20, %rsp 释放影子空间
 // 转换到Microsoft x64 calling convention
 0x59, // pop %rcx 出栈第一个参数
 0x5a, // pop %rdx 出栈第二个参数
 0x41, 0x58, // pop %r8 // 出栈第三个参数
 0x41, 0x59, // pop %r9 // 出栈第四个参数
 // 调用目标函数
 0x48, 0x83, 0xec, 0x20, // sub $0x20, %esp 预留32位的影子空间
 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs 0, %rax
 0xff, 0xd0, // callq *%rax 调用模拟的函数
 0x48, 0x83, 0xc4, 0x30, // add $0x30, %rsp 释放影子空间和参数(影子空间32 + 参数8*2)
 0x50, // push %rax 保存返回值
 // 调用getOriginalReturnAddress获取原返回地址
 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
 0xff, 0xd0, // callq *%rax 调用getOriginalReturnAddress
 0x48, 0x89, 0xc1, // mov %rax, %rcx 原返回地址存到rcx
 0x58, // 恢复返回值
 0x51, // 原返回地址入栈顶
 0xc3 // 返回
 };
 const int generic_func_loader_set_addr_offset = 18;
 const int generic_func_loader_target_offset = 44;
 const int generic_func_loader_get_addr_offset = 61;
 }
 // 获取动态链接函数的调用地址
 std::shared_ptr<void> resolveLibraryFunc(const std::string& name) {
 void* funcPtr = nullptr;
 if (name == "__libc_start_main") {
 funcPtr = __libc_start_main;
 }
 else if (name == "printf") {
 funcPtr = printf;
 }
 else {
 return nullptr;
 }
 void* addr = ::VirtualAlloc(nullptr,
 sizeof(generic_func_loader), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
 if (addr == nullptr) {
 throw std::runtime_error("allocate memory for _libc_start_main_loader failed");
 }
 std::shared_ptr<void> result(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
 std::memcpy(addr, generic_func_loader, sizeof(generic_func_loader));
 char* addr_c = reinterpret_cast<char*>(addr);
 *reinterpret_cast<void**>(addr_c + generic_func_loader_set_addr_offset) = setOriginalReturnAddress;
 *reinterpret_cast<void**>(addr_c + generic_func_loader_target_offset) = funcPtr;
 *reinterpret_cast<void**>(addr_c + generic_func_loader_get_addr_offset) = getOriginalReturnAddress;
 return result;
 }
}

理解这段代码需要先了解什么是x86 calling conventions, 在汇编中传递函数参数的办法由很多种, 像cdecl是把所有参数都放在栈中从低到高排列, 而fastcall是把第一个参数放ecx, 第二个参数放edx, 其余参数放栈中.

我们需要模拟的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()类型的函数指针再执行即可,

至此示例程序完成了构想中的所有功能.

执行效果如下图

如何实现在Windows上运行Linux程序,附示例代码

 

这份示例程序还有很多不足, 例如未支持32位Linux程序, 不支持加载其他Linux动态链接库(so), 不支持命令行参数等等.

而且这份示例程序和Bash On Windows的原理有所出入, 因为在用户层是无法模拟syscall.

我希望它可以让你对如何运行其他系统的可执行文件有一个初步的了解, 如果你希望更深入的了解如何模拟syscall, 可以查找rdmsr和wrmsr指令相关的资料.

最后附上我在编写这份示例程序中查阅的链接:

  • https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
  • https://en.wikipedia.org/wiki/X86_calling_conventions
  • http://refspecs.linuxbase.org/elf/elf.pdf
  • https://github.com/aeste/binutils/blob/develop/elfcpp/elfcpp.h
  • https://github.com/aeste/binutils/blob/develop/include/elf/external.h

纠错(2017-10-28), 用户层通过vsyscall机制是可以模拟syscall的.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何实现在的Windows上运行的Linux程序(附示例代码) 的相关文章

  • Qt 支持 Windows 蓝牙 API 吗?

    谁能告诉我 Qt 是否支持 Windows 蓝牙 API 如果是这样 您能否分享一些有关如何使用它的信息 自上次答复以来 这个问题的答案发生了一些变化 Qt 5 2 版为 Linux BlueZ 和 BlackBerry 设备实现了蓝牙 A
  • 相当于Linux中的导入库

    在 Windows C 中 当您想要链接 DLL 时 您必须提供导入库 但是在 GNU 构建系统中 当您想要链接 so 文件 相当于 dll 时 您就不需要链接 为什么是这样 是否有等效的 Windows 导入库 注意 我不会谈论在 Win
  • 如何从Windows阻止社交媒体[关闭]

    Closed 这个问题需要调试细节 help minimal reproducible example 目前不接受答案 我想根据时间阻止我的电脑上的社交媒体 晚上 9 点后屏蔽 上午 11 点后解锁 如家长控制 我尝试过关注但失败了 创建了
  • 不在焦点时响应键盘? (C#、Vista)

    我正在尝试编写一个应用程序 只要按下 Shift 键 无论当前哪个应用程序具有焦点 它都会做出响应 我尝试过这个SetWindowsHookEx 与GetKeyboardState 但这两种方法仅在应用程序窗口具有焦点时才有效 我需要它在全
  • 在哪里可以获得 PHP 5.3+ 的 runkit DLL 扩展?

    这是一个简单的问题 我在哪里可以获得 PHP 5 3 版本的 runkit 扩展 它的手册 http php net manual en book runkit php http php net manual en book runkit
  • NodeJS Express Windows 最大连接数设置

    在哪里设置nodejs的最大连接数 用于使用express get 在 Windows 10 中 与linux中的最大文件 描述符 设置有关吗 有该设置的 Windows 版本吗 最好是在nodejs中进行设置 以便在迁移到unix时兼容
  • SetCurrentDirectoryW 中的错误 206

    在我之后之前不清楚的问题 https stackoverflow com questions 44389617 long path name in setcurrentdirectoryw 我以某种方式能够创建一个具有长路径名的目录 但是
  • teracopy 如何替换默认的 Windows 副本

    我问了这个问题Windows 文件复制内部结构 动态加密 https stackoverflow com questions 24220382 windows file copy internals on the fly encryptio
  • Windows 操作系统中 ST_INO(os.stat() 输出)的含义

    谁能告诉我这个值的含义是什么st ino是跑步时os stat 在 Windows 上 Python 3 5 3 在早期的 Python 版本中 它包含虚拟值 但最近发生了变化 我找不到它是如何计算 生成的 我怀疑它因文件系统 NTFS F
  • 关闭有效句柄时,AppVerifier 报告“无效句柄 - 代码 c0000008”

    我有一个简单的测试程序 在运行时会失败并出现异常AppVerifier 程序重复STD INPUT HANDLE然后尝试使用关闭它CloseHandle 该程序运行良好 无需AppVerifier返回TRUE for CloseHandle
  • 获取自动热键中的可用屏幕区域

    我正在尝试编写一些简单的 AutoHotkey 脚本来移动窗口 但在获取正确的屏幕尺寸值时遇到问题 我试图获取屏幕上可用区域的大小 通常是全屏分辨率减去任务栏 也许还有任何其他停靠窗口 如 Vista 中的侧边栏 我发现的获取屏幕宽度的方法
  • 使用管理员权限打开cmd(Windows 10)

    我有自己的 python 脚本来管理我的计算机上的 IP 地址 它主要在命令行 Windows 10 中执行netsh命令 您必须具有管理员权限 这是我自己的计算机 我是管理员 运行脚本时我已经使用管理员类型的用户 Adrian 登录 我无
  • 为什么 Windows 命令 DIR 在搜索 *.tif 文件时也会输出 *.tiff 文件?

    我想使用 Windows 命令DIR为了找到唯一TIF文件 即具有扩展名的文件 tif 因此我使用以下小批处理文件 for f delims a IN dir b a d s C wolter testversion input tif d
  • C# 获取子窗口句柄

    我正在用 C 启动一个进程 然后使用 SendMessage 将 Windows 消息发送到该进程 通常我将消息发送到 Process MainWindowHandle 但在某些情况下 我可能需要找到子窗口句柄并向那里发送消息 我将如何在
  • 使用Windows 7计算器进行对数计算[关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我想使用Windows计算器在科学模式中为了求解一个非常基本的对数方程 但不幸的是 我无法做到这一点 问题是这样的 log 5 125 非常感谢您的帮
  • pyinstaller错误:OSError:[WinError 6]句柄无效

    该文件使用终端命令获取 wifi 密码netsh wlan show profiles我之前使用 pyinstaller 创建了一些 exe 它们工作得很好 代码 import subprocess import time import s
  • Delphi定时器比毫秒更精确

    我在 Delphi 中有一个程序 它以 25 赫兹 每秒 25 次 的速度从外部应用程序获取帧 然后通过创建 1 2 个额外帧将其转换为 60 赫兹 每秒 60 帧 我需要通过连续构建帧缓冲区并从单独的线程输出帧来输出这些额外的帧 问题是
  • 带有 for 循环和管道的批处理脚本

    我想要一个目录中的所有 csv 文件 其文件名不包含单词 summary 在命令提示符下我可以键入以下命令 dir b my dir csv find V summary 当我尝试将上述命令传输到批处理文件中时 我遇到了一个问题 因为 fo
  • 具有多处理功能的 Python 代码无法在 Windows 上运行

    以下简单的绝对初学者代码在 Ubuntu 14 04 Python 2 7 6 和 Cygwin Python 2 7 8 上运行 100 但在 Windows 64 位 Python 2 7 8 上挂起 我使用另一个片段观察到了同样的情况
  • 处理器关联组 C#

    我使用的是 72 核的 Windows Server 2016 我看到有两组处理器 我的 net 应用程序将使用一个或其他组 我需要能够强制我的应用程序使用我选择的组 我看到下面的代码示例 但我无法使其工作 我可能传递了错误的变量 我希望应

随机推荐

  • Mysql多对多关系,分组拼接把多个数据查询到一条数据上

    GROUP CONCAT str 分组字符串拼接 与分组一起使用 案例 查询企业信息以及企业分类信息 其中企业分类信息和企业是多对多的关系 按普通的联表查询 我们会查询到一条企业信息对应多个企业分类 会出现多个记录 如果想实现把同一个企业的
  • 全面理解java中的构造方法以及this关键字的用法(超详细)

    Hello 各位铁汁们 我是小 儿哈 今天我又来更新我的Java基础学习博客了 本篇主要内容概述 1 如何用构造方法初始化对象 2 为啥要有this这个关键字 3 this 属性名访问成员变量 成员方法 4 this 方法名 this 的用
  • Pandas进阶筛选和取数操作

    总结了pandas各种进阶操作与使用技巧 并且对各方法间的效率进行比较 创建一个pandas的dataframe对象作为下文样例 import pandas as pd import numpy as np df pd DataFrame
  • 陀螺研究院:《2019年分布式金融商业趋势及落地情况分析报告》

    2018年末开始 以DeFi为代表的分布式金融 在业内引起了广泛的讨论 传统的金融模式以中央银行 商业银行 非银金融机构为核心展开支付 借贷 保险等场景内的应用 但DeFi彻底摆脱了原有的核心 以分布式账本作为清算依据 从而降低了金融服务中
  • 前馈全连接神经网络和函数逼近、时间序列预测、手写数字识别

    https www cnblogs com conmajia p annt feed forward fully connected neural networks html 前馈全连接神经网络和函数逼近 时间序列预测 手写数字识别 And
  • springboot中JDBC连接超时问题

    最近项目中有一个问题 电子保卡信息要写入数据库 但写入失败 报错 息是这样的 The last packet successfully received from the server was 57 704 088 milliseconds
  • Stream流

    Stream流 Stream 流 是一个来自数据源的元素队列并支持聚合操作 元素是特定类型的对象 形成一个队列 Java中的Stream并不会存储元素 而 是按需计算 数据源 流的来源 可以是集合 数组等 聚合操作 类似SQL语句一样的操作
  • Bes 充电盒协议总结

    1 开盖 上升沿信号开机 a 充电脚设成3 0 v 然后延迟160ms b 充电脚设成5v 然后延时100 ms c充电脚设成3 0 v 2 合盖 a 开5v 然后延时3s b 关5v 然后延时45ms c 发送复位pattern 0101
  • c++ 字符串相等比较

    介绍 在C 中比较字符串的技术 Techniques to Compare Strings in C Strings in C can be compared using either of the following techniques
  • mysql命令 show_mysql--SHOW命令大全

    SHOW AUTHORS 顾名思义 这个要展示的是各位MYSQL开发者的信息 包括姓名 住址及相关注解 e g 1 mysql gt show authors G 1 row Name Brian Krow Aker Location Se
  • LeetCode 62. Unique Paths

    题目链接 题目描述 A robot is located at the top left corner of a m x n grid marked Start in the diagram below The robot can only
  • Microsoft Store无法打开解决方案 错误代码:0x80131500

    这种情况大部分是设置了Vpn代理 提供两种解决方案 一 打开 运行 输入 inetcpl cpl 点还原高级设置 注意看看勾选了TLS 1 2没有 二 如果上述方法没有解决 那么就打开Internet选项 gt 安全选项卡 gt 点一下 将
  • pip安装opencv-python

    文章目录 前言 一 基本概念 二 操作步骤 1 删除旧版本 2 pip升级 3 opencv python安装 总结 前言 OpenCV的全称是Open Source Computer Vision Library 是一个跨平台的计算机视觉
  • 跳转至tabBar页面不触发页面的onLoad,点击底部tabar不触发onLoad

    小程序想跳转tabar页面带参数 使用了全局变量app js的全局 跳转到页面后发现不是每次都执行onLoad方法 传参失败 更换跳转的方法解决 由wx switchTab改为wx reLaunch 就可以了 点击底部导航不触发解决 js
  • Ubuntu挂载Win10下的NTFS硬盘出错的解决方案

    概述 在Ubuntu下打开Win10的NTFS硬盘总是提示出错了 而且是全部的NTFS盘都出错 其中sdb1错误显示如下 he disk contains an unclean file system 0 0 Metadata kept i
  • matplotlib函数总结

    导入matplotlib import matplotlib pyplot as plt import matplotlib Figures对象包含一个或多个Asex对象 方法 matplotlib rc figure figsize 14
  • 在Ubuntu18.04.3系统中安装谷歌拼音输入法(Google Pinyin)

    一 安装前的准备 在Ubuntu18 04下 谷歌拼音输入法是基于Fcitx输入法的 因此 我们需要首先安装Fcitx 一般来说 Ubuntu最新版中都默认安装了Fcitx 但是为了确保一下 我们可以在系统终端中运行如下命令 sudo ap
  • 如何用PHP解决高并发与大流量问题

    举个例子 高速路口 1秒钟来5部车 每秒通过5部车 高速路口运作正常 突然 这个路口1秒钟只能通过4部车 车流量仍然依旧 结果必定出现大塞车 5条车道忽然变成4条车道的感觉 同理 某一个秒内 20 500个可用连接进程都在满负荷工作中 却仍
  • StrangeIOC中Signal类使用详解

    在讲解Signal类之前 先复习一下dispatch的用法 1 View层调用自身的dispatch view 告知绑定的Mediator层也调用自身的dispatch mediator 2 Mediator层的dispatch media
  • 如何实现在的Windows上运行的Linux程序(附示例代码)

    而今天的这篇文章将会讲解如何自己实现一个简单的原生Linux程序运行器 这个运行器在用户层实现 原理和Bash On Windows不完全一样 比较接近Linux上的Wine 示例程序完整的代码在github上 地址是 https gith