如果我针对x86架构编译我的C/C++程序,似乎同一程序应该运行在任何具有相同架构的计算机上。
这是真的,但也有一些细微差别。
从C语言的角度来看,让我们考虑几个与操作系统无关的程序。
假设您的程序从一开始所做的就是压力测试--通过在没有任何I/O的情况下进行大量计算来测试CPU。对于所有的OSes,机器代码可以完全相同(只要它们都在相同的CPU模式下运行,例如x86 32位保护模式)。您甚至可以直接用汇编语言编写它,不需要对每个操作系统进行调整。
但是,每个操作系统都需要包含此代码的二进制文件的不同头部。例如,Windows需要PE格式,Linux需要小精灵,macOS使用马赫-O格式。对于简单的程序,您可以将机器代码作为一个单独的文件来准备,并为每个OS的可执行格式准备一堆头文件。然后,您所需要的“重新编译”实际上是连接页眉和机器代码,并可能添加对齐“页脚”。
因此,假设您将C代码编译成机器代码,如下所示:
代码语言:javascript复制offset: instruction disassembly
00: f7 e0 mul eax
02: eb fc jmp short 00这是一个简单的压力测试代码,它自己重复地执行eax寄存器的乘法操作.
现在您想让它在32位Linux和32位Windows上运行。您需要两个头部,下面是示例(十六进制转储):
对于Linux:代码语言:javascript复制000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 >.ELF............<
000010 02 00 03 00 01 00 00 00 54 80 04 08 34 00 00 00 >........T...4...<
000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 28 00 >........4. ...(.<
000030 00 00 00 00 01 00 00 00 54 00 00 00 54 80 04 08 >........T...T...<
000040 54 80 04 08 04 00 00 00 04 00 00 00 05 00 00 00 >T...............<
000050 00 10 00 00 >....<对于Windows (*只重复前面的行,直到到达*下面的地址):代码语言:javascript复制000000 4d 5a 80 00 01 00 00 00 04 00 10 00 ff ff 00 00 >MZ..............<
000010 40 01 00 00 00 00 00 00 40 00 00 00 00 00 00 00 >@.......@.......<
000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
000030 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 >................<
000040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 >........!..L.!Th<
000050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f >is program canno<
000060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 >t be run in DOS <
000070 6d 6f 64 65 2e 0d 0a 24 00 00 00 00 00 00 00 00 >mode...$........<
000080 50 45 00 00 4c 01 01 00 ee 71 b4 5e 00 00 00 00 >PE..L....q.^....<
000090 00 00 00 00 e0 00 0f 01 0b 01 01 47 00 02 00 00 >...........G....<
0000a0 00 02 00 00 00 00 00 00 00 10 00 00 00 10 00 00 >................<
0000b0 00 10 00 00 00 00 40 00 00 10 00 00 00 02 00 00 >......@.........<
0000c0 01 00 00 00 00 00 00 00 03 00 0a 00 00 00 00 00 >................<
0000d0 00 20 00 00 00 02 00 00 40 fb 00 00 03 00 00 00 >. ......@.......<
0000e0 00 10 00 00 00 10 00 00 00 00 01 00 00 00 00 00 >................<
0000f0 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 >................<
000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
000170 00 00 00 00 00 00 00 00 2e 66 6c 61 74 00 00 00 >.........flat...<
000180 04 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00 >................<
000190 00 00 00 00 00 00 00 00 00 00 00 00 60 00 00 e0 >............`...<
0001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
000200现在,如果将您的机器代码附加到这些头文件中,并且对于Windows,还附加了一串空字节以使文件大小为1024字节,那么您将得到在相应的操作系统上运行的有效可执行文件。
假设现在您的程序想要在完成一定数量的计算之后终止。
现在它有两个选择:代码语言:javascript复制1. Crash—e.g. by execution of an invalid instruction (on x86 it could be `UD2`). This is easy, OS-independent, but not elegant.
2. Ask the OS to correctly terminate the process. At this point we need an OS-dependent mechanism to do this.
在x86 Linux上
代码语言:javascript复制xor ebx, ebx ; zero exit code
mov eax, 1 ; __NR_exit
int 0x80 ; do the system call (the easiest way)在x86 Windows 7上,它将是
代码语言:javascript复制 ; First call terminates all threads except caller thread, see for details:
; http://www.rohitab.com/discuss/topic/41523-windows-process-termination/
mov eax, 0x172 ; NtTerminateProcess_Wind7
mov edx, terminateParams
int 0x2e ; do the system call
; Second call terminates current process
mov eax, 0x172
mov edx, terminateParams
int 0x2e
terminateParams:
dd 0, 0 ; processHandle, exitStatus注意,在其他Windows版本中,您需要另一个系统调用号。调用NtTerminateProcess的正确方法是通过另一个依赖操作系统的细微差别:共享库。
现在,您的程序希望加载一些共享库,以避免重新发明一些车轮。好的,我们已经看到我们的可执行文件格式是不同的。假设我们已经考虑到这一点,并为针对每个目标OS的文件准备了导入部分。还有一个问题:调用函数的方式--所谓的呼叫约定-for--每个操作系统都是不同的。
例如,假设您的程序需要调用的C语言函数返回包含两个int值的结构。在Linux上,调用方必须分配一些空间(例如在堆栈上),并将指向它的指针作为被调用函数的第一个参数传递,如下所示:
代码语言:javascript复制sub esp, 12 ; 4*2+alignment: stack must be 16-byte aligned
push esp ; right before the call instruction
call myFunc在Windows上,您可以在不向函数传递任何附加参数的情况下在EAX中获得结构的第一个int值,在EDX中获得第二个值。
还有其他一些细微之处,如不同的名称残缺方案(尽管在相同的操作系统上编译器之间也会有所不同)、不同的数据类型(例如long double上的MSVC与GCC上的long double )等,但从编译器和链接器的角度来看,上述这些都是OSes之间最重要的区别。