tangptr@126.com 发表于 2026-3-2 11:16:02

【娱乐】用Rust给Windows XP编程

本帖最后由 tangptr@126.com 于 2026-4-12 00:13 编辑

# 前言
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`?[请参考此文](https://www.0xaa55.com/thread-27602-1-1.html)。
除了`print`之外,还有就是:我™怎么用`Vec`、`String`这种动态大小的数据类型?[请参考此文](https://www.0xaa55.com/thread-27603-1-1.html)。
实现`GlobalAlloc` trait的时候直接用`HeapAlloc`/`HeapFree`/`HeapReAlloc`这些API就行了,没必要非得折腾像`jemalloc`,`mimalloc`那些复杂的分配器,除非内存分配出现了显著的性能瓶颈。
不幸的是,`HashMap`所使用的哈希算法(https://abseil.io/blog/20180927-swisstables)仅在`std`中可用。如果需要使用KV存储类型但不想自己造轮子,只能选择[`BTreeMap`](https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html)了,性能从`O(1)`变成了`O(logn)`。

## 入口函数以及命令行
通常而言,入口函数可以定义为:
```Rust
# extern "C" fn start(peb:*const PEB)->NTSTATUS
{
        ......
}
```
有了`peb`之后,可以通过`(*(*peb).ProcessParameters).CommandLine`来获取一整条命令。注意:
- 它没有被`split`过!可以用(https://crates.io/crates/arg)这个crate来帮你split命令行。
- 它是UTF-16的字符串!可以用`String::from_utf16_lossy`函数把它转成Rust原生支持的UTF-8字符串。

但是,在XP上入口函数没有PEB这个参数。只能用内联汇编来获取PEB了:
```Rust
# extern "C" fn start()->NTSTATUS
{
        let peb=unsafe
        {
                let v:*const PEB;
                asm!
                (
                        "mov {},fs:",
                        out(reg) v
                );
                v
        };
        ......
}
```
不得不说Rust的内联汇编从感觉上比GCC的内联汇编要顺手得多。(不仅仅是GCC默认AT&T语法的问题)

# 编译
直接编译会因为链接器问题导致编译出错。我们需要在项目的根目录下创建一个`build.rs`文件作为编译脚本,并构造一个`main`函数:
```Rust
fn main()
{

}
```
接下来就是往`main`函数里填东西了。

## 忽略默认LIB
否则`link.exe`就会去链接一堆默认的CRT。在`build.rs`的`main`函数里加上一句:
```Rust
println!("cargo:rustc-link-arg=/NODEFAULTLIB");
```

## 缺少入口函数
由于我们在程序里指定了`#!`和`#!`,因此`cargo`不会给链接器指定入口函数。在`build.rs`的`main`函数里加上一句:
```Rust
println!("cargo:rustc-link-arg=/ENTRY:start");
```

## 缺少依赖
链接器极有可能会报出缺少`memcpy`,`memset`,`memcmp`,`strlen`函数的错误。解决办法是直接引用`ntdll.dll`里导出的函数。由于我们还用了`WriteConsoleW`和`HeapAlloc`什么的,顺便加上`kernel32.dll`吧。
在`build.rs`的`main`函数里加上:
```Rust
println!("cargo:rustc-link-lib=ntdllp");
println!("cargo:rustc-link-lib=kernel32");
```

## 缺少`__CxxFrameHandler3`
我就从来没见过这个函数会被调用到,用IDA看的时候,没有任何东西会引用它。它的实现留空就行。
此外`ntdllp.lib`的`loadcfg.obj`里还会用到`__security_cookie`这个全局变量,顺便给他带上。
把以下代码放进程序代码里,不是`build.rs`里!
```Rust
# extern "C" fn __CxxFrameHandler3()
{
       
}

# static __security_cookie:u32=0;
```

## 子系统版本
由于链接器版本太新(从VC2012开始),它生成的PE文件的可选头里,子系统版本(Subsystem Version)默认是6.00。这会导致Windows XP报出“EXE不是有效的Win32应用程序”的错误。这并不意味着我们生成了Win64的应用程序,而单纯是因为子系统版本太高了。只需要将其指定为5.01,这样Windows XP就认识了。
在`build.rs`的`main`函数里加上一句:
```Rust
println!("cargo:rustc-link-arg=/SUBSYSTEM:CONSOLE,5.01");
```

## 禁止使用AVX
Windows XP在开发的时候,根本就不认识AVX指令集。于是XP在设置`xcr0`寄存器时,不会启用AVX扩展指令,因此需要禁止编译器生成出AVX指令!
在项目根目录下创建`.cargo`目录,并在其中创建`config.toml`文件:
```toml

# Windows XP does not enable AVX extensions!
rustflags = ["-Ctarget-feature=-avx"]
```

# 如果真的要用std
如果你真的需要std,那么(https://crates.io/crates/thunk-rs)库可以替你解决掉`api-ms-win-core-synch-l1-2-0`的导入。它的作用是当新系统的API无法使用的时候,手动实现这些API的功能。这样一来即便是`std`的东西也能在XP里用了。但注意,`thunk-rs`和`YY-Thunks`的作者不是同一个人,并且`thunk-rs`引用的版本有点老了。

# 结语
这年头愿意支持Windows 7的程序都少之又少了,要支持Windows XP简直就是笑话,**但你永远无法想象你会遇到什么样的逆天用户**。

AyalaRs 发表于 2026-3-3 09:36:24

获取peb可以用api,ntcurrentteb从teb取,栈安全也要读teb

YY菌 发表于 2026-3-4 08:58:18

本帖最后由 YY菌 于 2026-3-4 09:03 编辑

__CxxFrameHandler3和__security_cookie问题可以用WDK里面的msvcrt.lib,这样链接出来的exe是用的系统内痔CRT msvcrt.dll,只不过XP的这个CRT版本太老了只有6.0(Win7有9.0),可以考虑再用WDK7里面的msvcrt_xp.obj来静态补充6.0~9.0期间的新增内容。
不过对于弹幕流来说的话:你™又用屎山了! :lol

YY菌 发表于 2026-3-4 09:03:08

AyalaRs 发表于 2026-3-3 09:36
获取peb可以用api,ntcurrentteb从teb取,栈安全也要读teb

TEB可以直接从fs/gs寄存器拿(x86走fs寄存器、x64走gs寄存器),其实NtCurrentTeb内部也是这样获取的,不过VC下NtCurrentTeb好像默认被替换为直接读寄存器了。WinSDK没有提供RtlGetCurrentPeb,可以自己声明这个函数然后链接ntdll.lib,但是这样又依赖“屎山”了,所以还是可以自己学NtCurrentTeb的方式替换为直接从fs/gs寄存器取PEB:lol

AyalaRs 发表于 2026-3-4 19:32:00

本帖最后由 AyalaRs 于 2026-3-4 19:49 编辑

YY菌 发表于 2026-3-4 09:03
TEB可以直接从fs/gs寄存器拿(x86走fs寄存器、x64走gs寄存器),其实NtCurrentTeb内部也是这样获取的,不 ...

NtCurrentTeb 即是宏又是api,和编译环境有关系,读写fs gs的宏好像还蛮多的,内联汇编多多少少有点小问题,更喜欢外联(平时调试除外),内联汇编经常要配合条件编译,代码文件阅读起来就变得很难受

YY菌 发表于 2026-3-5 14:20:23

AyalaRs 发表于 2026-3-4 19:32
NtCurrentTeb 即是宏又是api,和编译环境有关系,读写fs gs的宏好像还蛮多的,内联汇编多多少少有点小问 ...

是啊,所以现在巨硬推荐用intrinsic函数,而不是内联汇编。

bwz26452938 发表于 2026-3-7 09:05:49

用户逆天,客户也逆天,做乙方太难了,就因为客户还有XP的机器,导致老的技术栈难以割舍,心智成本陡然飙升

YY菌 发表于 2026-3-9 08:43:58

bwz26452938 发表于 2026-3-7 09:05
用户逆天,客户也逆天,做乙方太难了,就因为客户还有XP的机器,导致老的技术栈难以割舍,心智成本陡然飙升 ...

甲方要求越高,乙方收费越高不就行了。:lol

tangptr@126.com 发表于 2026-3-13 00:46:00

YY菌 发表于 2026-3-5 14:20
是啊,所以现在巨硬推荐用intrinsic函数,而不是内联汇编。


Rust还没有这样的intrinsics,只能用内联汇编了。好在包装一下也不难,我在帖子里也说了,Rust内联汇编比GCC好写的多。如果非要把读TEB包装一下的话:

```Rust
unsafe fn readtebusize(offset:usize)->usize
{
    use core::arch::asm;
    unsafe
    {
      let v:usize;
      #
      asm!("mov {},fs:[{}]",out(reg) v,in(reg) offset);
      #
      asm!("mov {},gs:[{}]",out(reg) v,in(reg) offset);
      v
    }
}
```
此外windows-sys这个crate没有提供`NtCurrentTeb`。

YY菌 发表于 2026-3-13 08:45:14

tangptr@126.com 发表于 2026-3-13 00:46

Rust还没有这样的intrinsics,只能用内联汇编了。好在包装一下也不难,我在帖子里也说了,Rust内联 ...

包装内联汇编不是会导致函数失去内联吗?还有就是寄存器的分配就只能写死,intrinsic函数可以编译器来自动选择什么寄存器。尤其是包装成返回值来返回结果,内联会就只能固定使用eax/rax寄存器:L

tangptr@126.com 发表于 2026-3-13 12:33:31

YY菌 发表于 2026-3-13 08:45
包装内联汇编不是会导致函数失去内联吗?还有就是寄存器的分配就只能写死,intrinsic函数可以编译器来自 ...

你看我写的例子,并没有写死要用什么寄存器。只使用`{}`来表示放进去什么值,所以编译器能自动分配。至于返回值,你看我在内联汇编里修改`rax`寄存器了么。
GCC的内联汇编也差不多,可以用`%1`,`%0`啥的,不会写死寄存器,编译器也能优化。对于简单的内联汇编,大部分人估计会选择直接给它`#define`成一个宏。
也就MSVC的内联汇编比较操蛋,只能写死寄存器了。

YY菌 发表于 2026-3-14 13:43:41

tangptr@126.com 发表于 2026-3-13 12:33
你看我写的例子,并没有写死要用什么寄存器。只使用`{}`来表示放进去什么值,所以编译器能自动分配。 ...

原来如此,那如果是%1 %2 这种用到的数量超过了寄存器上限了呢?
页: [1]
查看完整版本: 【娱乐】用Rust给Windows XP编程