为什么每个操作系统都需要重新编译C/C++?

为什么每个操作系统都需要重新编译C/C++?

如果我针对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之间最重要的区别。

相关推荐

如何查看电脑内存条信息?
365bet提款条件

如何查看电脑内存条信息?

🗓️ 09-29 👁️ 3789
脖子红是什么原因
365bet是什么

脖子红是什么原因

🗓️ 09-12 👁️ 1508
如何发长微博:一篇全面的攻略,助你突破字数限制
【教程】饭卡水卡IC卡复制
趣投必发365

【教程】饭卡水卡IC卡复制

🗓️ 10-29 👁️ 3659
jmp软件与minitab软件的区别
365bet提款条件

jmp软件与minitab软件的区别

🗓️ 08-18 👁️ 3828
炉窑工程公司注册需要什么条件(成立一家炉窑工程公司要多少钱)
2025最新黑色贵烟价格表和图片查询
365bet是什么

2025最新黑色贵烟价格表和图片查询

🗓️ 07-19 👁️ 7900
UC浏览器为什么打不开某些网页
趣投必发365

UC浏览器为什么打不开某些网页

🗓️ 09-30 👁️ 152
点蚊香睡觉会不会中毒 蚊香多久能把蚊子熏死
趣投必发365

点蚊香睡觉会不会中毒 蚊香多久能把蚊子熏死

🗓️ 08-28 👁️ 6179
2025年最值得信赖的10大登山鞋品牌推荐,选购指数超高!
百威啤酒的度数一览 百威啤酒生产全过程分享
趣投必发365

百威啤酒的度数一览 百威啤酒生产全过程分享

🗓️ 07-03 👁️ 8350
丰巢快递柜怎么使用?
趣投必发365

丰巢快递柜怎么使用?

🗓️ 09-06 👁️ 1859