32 位二进制是如何跑在 64 位操作系统上的
现在电脑上的 CPU 和操作系统都是 64 位的了,x86 架构扩展到 64 位带来了很多好处,比如:更大的地址空间让你可以直接 mmap 一个几百 GB 的文件;更多的寄存器让内存访问减少,应用性能更好。但还是有很多应用程序是 32 位的,特别是 windows 上那些闭源软件,即使一直在更新,也还是 32 位的。那么问题来了: 32 位的 x86 二进制是怎么在 64 位 CPU 和操作系统上运行的呢?

现在电脑上的 CPU 和操作系统都是 64 位的了,x86 架构扩展到 64 位带来了很多好处,比如:更大的地址空间让你可以直接 mmap 一个几百 GB 的文件;更多的寄存器让内存访问减少,应用性能更好。但还是有很多应用程序是 32 位的,特别是 windows 上那些闭源软件,即使一直在更新,也还是 32 位的。那么问题来了: 32 位的 x86 二进制是怎么在 64 位 CPU 和操作系统上运行的呢?实际上,操作系统和 CPU 分别在软件和硬件上都做了不少工作。

操作系统的接口兼容

Windows 上的兼容层是 WoW64, 它的主要功能包括:

  1. 分配进程的虚拟地址空间时,只使用 32 位地址能寻址的范围。

  2. 在应用调用 windows API 时,做 32 位和 64 位之间的结构体双向翻译。

而 Linux 上的做法是在内核里保留了原来的 32 位系统调用 (syscall_32.tbl), 那么 userland 里:

  • 32 位的静态连接 ELF 直接加载在 32 位以下的地址空间,继续使用原来的系统调用,和以前在 32 位内核上运行没什么区别

  • 动态连接的 ELF 根据 .interp 里面指定的 ld.so 加载。 32 位的 ELF 指定的是 32 位的 ld.so, 还是和以前一样。

CPU 的兼容

首先需要明确一点, x86 的机器码和 x86-64 是不兼容的。不兼容的地方包括:

  1. 一些 x86 上的不常用指令被移除了1
  2. 一些机器码被修改了用途 2

例如:机器码序列 48 ff c0 中的第一个字节 48 就是被修改了用途的机器码。在 x86 上, 48inc %eax, 整个序列解码为

inc %eax
dec %eax

而在 x86-64 上, 48 改成了 REX Prefix, 用来修饰后面 ff c0 的操作数长度,整个序列解码为

inc %rax

另外,在指令没有用 REX Prefix 修饰操作数长度时,默认的操作数长度也是不一样的。3

因此, CPU 必须明确地知道当前执行的机器码是 x86 的还是 x86-64 的。实际上,这是通过 code segment descriptor 中的 一个 flag 来确定的。执行 x86-64 代码的模式称为 64-bit submode; 执行 x86 代码的模式称为 compatibility submode. 操作系统在初始化时就会在 GDT 中准备好 x86 和 x86-64 两个版本的 code segment descriptors, 根据需要进行切换。这里的切换时机在 wow64 和 linux 有区别。

WOW64 的兼容层在 userland, 32 位应用调用系统 API 时,在兼容层里完成模式切换,这大概需要用到 long jmp 或 long call 指令来切换 CS 寄存器。而 linux 运行 32 位应用时整个 userland 都是 32 位的,模式切换是通过在 sysenter 的时候切换到内核的 CS 寄存器来完成的。

总结

在 64 位操作系统上运行 32 位二进制是需要软硬件两方面的支持的。通过 CS 寄存器选择 long mode 中的 compatibility submod, x86-64 CPU 才能执行 x86 的机器码。同时软件方面需要操作系统提供在 32 位的应用和 64 位的系统 API 之间的调用机制。


最后修改于 2025-05-24