前言
Windows XP在12年前(2014年4月8日)的时候微软就已经停止支持了,本文写出来纯属娱乐。
准备
工欲善其事,必先利其器。正常情况下Rust默认的工具链是x86_64-pc-windows-msvc,这编译出来的是64位程序,但是一般意义上的XP是32位的,所以要另外安装i686-pc-windows-msvc工具链。
rustup target add i686-pc-windows-msvc
在编译程序时,不能简单的用cargo build命令,而是要改成cargo build --target i686-pc-windows-msvc了。
使用no_std
Rust的std库在兼容性上非常激进,它链接的外部DLL连Windows 7都不支持(如api-ms-win-core-synch-l1-2-0,以l1-2-0结尾的几乎没有一个支持Windows 7的)。因此必须要放弃使用Rust的std。
舍弃std之后,一个很明显的问题就来了:我™怎么print?请参考此文。
除了print之外,还有就是:我™怎么用Vec、String这种动态大小的数据类型?请参考此文。
实现GlobalAlloc trait的时候直接用HeapAlloc/HeapFree/HeapReAlloc这些API就行了,没必要非得折腾像jemalloc,mimalloc那些复杂的分配器,除非内存分配出现了显著的性能瓶颈。
不幸的是,HashMap所使用的哈希算法SwissTable仅在std中可用。如果需要使用KV存储类型但不想自己造轮子,只能选择BTreeMap了,性能从O(1)变成了O(logn)。
入口函数以及命令行
通常而言,入口函数可以定义为:
#[unsafe(no_mangle)] extern "C" fn start(peb:*const PEB)->NTSTATUS
{
......
}
有了peb之后,可以通过(*(*peb).ProcessParameters).CommandLine来获取一整条命令。注意:
- 它没有被
split过!可以用arg这个crate来帮你split命令行。
- 它是UTF-16的字符串!可以用
String::from_utf16_lossy函数把它转成Rust原生支持的UTF-8字符串。
但是,在XP上入口函数没有PEB这个参数。只能用内联汇编来获取PEB了:
#[unsafe(no_mangle)] extern "C" fn start()->NTSTATUS
{
let peb=unsafe
{
let v:*const PEB;
asm!
(
"mov {},fs:[0x30]",
out(reg) v
);
v
};
......
}
不得不说Rust的内联汇编从感觉上比GCC的内联汇编要顺手得多。(不仅仅是GCC默认AT&T语法的问题)
编译
直接编译会因为链接器问题导致编译出错。我们需要在项目的根目录下创建一个build.rs文件作为编译脚本,并构造一个main函数:
fn main()
{
}
接下来就是往main函数里填东西了。
忽略默认LIB
否则link.exe就会去链接一堆默认的CRT。在build.rs的main函数里加上一句:
println!("cargo:rustc-link-arg=/NODEFAULTLIB");
缺少入口函数
由于我们在程序里指定了#![no_std]和#![no_main],因此cargo不会给链接器指定入口函数。在build.rs的main函数里加上一句:
println!("cargo:rustc-link-arg=/ENTRY:start");
缺少依赖
链接器极有可能会报出缺少memcpy,memset,memcmp,strlen函数的错误。解决办法是直接引用ntdll.dll里导出的函数。由于我们还用了WriteConsoleW和HeapAlloc什么的,顺便加上kernel32.dll吧。
在build.rs的main函数里加上:
println!("cargo:rustc-link-lib=ntdllp");
println!("cargo:rustc-link-lib=kernel32");
缺少__CxxFrameHandler3
我就从来没见过这个函数会被调用到,用IDA看的时候,没有任何东西会引用它。它的实现留空就行。
此外ntdllp.lib的loadcfg.obj里还会用到__security_cookie这个全局变量,顺便给他带上。
把以下代码放进程序代码里,不是build.rs里!
#[unsafe(no_mangle)] extern "C" fn __CxxFrameHandler3()
{
}
#[unsafe(no_mangle)] static __security_cookie:u32=0;
子系统版本
由于链接器版本太新(从VC2012开始),它生成的PE文件的可选头里,子系统版本(Subsystem Version)默认是6.00。这会导致Windows XP报出“EXE不是有效的Win32应用程序”的错误。这并不意味着我们生成了Win64的应用程序,而单纯是因为子系统版本太高了。只需要将其指定为5.01,这样Windows XP就认识了。
在build.rs的main函数里加上一句:
println!("cargo:rustc-link-arg=/SUBSYSTEM:CONSOLE,5.01");
禁止使用AVX
Windows XP在开发的时候,根本就不认识AVX指令集。于是XP在设置xcr0寄存器时,不会启用AVX扩展指令,因此需要禁止编译器生成出AVX指令!
在项目根目录下创建.cargo目录,并在其中创建config.toml文件:
[build]
# Windows XP does not enable AVX extensions!
rustflags = ["-Ctarget-feature=-avx"]
如果真的要用std
如果你真的需要std,那么thunk-rs库可以替你解决掉api-ms-win-core-synch-l1-2-0的导入。它的作用是当新系统的API无法使用的时候,手动实现这些API的功能。这样一来即便是std的东西也能在XP里用了。但注意,thunk-rs和YY-Thunks的作者不是同一个人,并且thunk-rs引用的版本有点老了。
结语
这年头愿意支持Windows 7的程序都少之又少了,要支持Windows XP简直就是笑话,但你永远无法想象你会遇到什么样的逆天用户。