找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 382|回复: 11

【娱乐】用Rust给Windows XP编程

[复制链接]
发表于 2026-3-2 11:16:02 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
本帖最后由 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请参考此文
除了print之外,还有就是:我™怎么用VecString这种动态大小的数据类型?请参考此文
实现GlobalAlloc trait的时候直接用HeapAlloc/HeapFree/HeapReAlloc这些API就行了,没必要非得折腾像jemallocmimalloc那些复杂的分配器,除非内存分配出现了显著的性能瓶颈。
不幸的是,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.rsmain函数里加上一句:

println!("cargo:rustc-link-arg=/NODEFAULTLIB");

缺少入口函数

由于我们在程序里指定了#![no_std]#![no_main],因此cargo不会给链接器指定入口函数。在build.rsmain函数里加上一句:

println!("cargo:rustc-link-arg=/ENTRY:start");

缺少依赖

链接器极有可能会报出缺少memcpy,memset,memcmpstrlen函数的错误。解决办法是直接引用ntdll.dll里导出的函数。由于我们还用了WriteConsoleWHeapAlloc什么的,顺便加上kernel32.dll吧。
build.rsmain函数里加上:

println!("cargo:rustc-link-lib=ntdllp");
println!("cargo:rustc-link-lib=kernel32");

缺少__CxxFrameHandler3

我就从来没见过这个函数会被调用到,用IDA看的时候,没有任何东西会引用它。它的实现留空就行。
此外ntdllp.libloadcfg.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.rsmain函数里加上一句:

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-rsYY-Thunks的作者不是同一个人,并且thunk-rs引用的版本有点老了。

结语

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

回复

使用道具 举报

发表于 2026-3-3 09:36:24 | 显示全部楼层
获取peb可以用api,ntcurrentteb从teb取,栈安全也要读teb
回复 赞! 靠!

使用道具 举报

发表于 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期间的新增内容。
不过对于弹幕流来说的话:你™又用屎山了!
回复 赞! 靠!

使用道具 举报

发表于 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
回复 赞! 靠!

使用道具 举报

发表于 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的宏好像还蛮多的,内联汇编多多少少有点小问题,更喜欢外联(平时调试除外),内联汇编经常要配合条件编译,代码文件阅读起来就变得很难受
回复 赞! 靠!

使用道具 举报

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

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

使用道具 举报

发表于 2026-3-7 09:05:49 | 显示全部楼层
用户逆天,客户也逆天,做乙方太难了,就因为客户还有XP的机器,导致老的技术栈难以割舍,心智成本陡然飙升
回复 赞! 靠!

使用道具 举报

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

甲方要求越高,乙方收费越高不就行了。
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2026-3-13 00:46:00 | 显示全部楼层
YY菌 发表于 2026-3-5 14:20
是啊,所以现在巨硬推荐用intrinsic函数,而不是内联汇编。

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

unsafe fn readtebusize(offset:usize)->usize
{
    use core::arch::asm;
    unsafe
    {
        let v:usize;
        #[cfg(target="x86")]
        asm!("mov {},fs:[{}]",out(reg) v,in(reg) offset);
        #[cfg(target="x86_64")]
        asm!("mov {},gs:[{}]",out(reg) v,in(reg) offset);
        v
    }
}

此外windows-sys这个crate没有提供NtCurrentTeb

回复 赞! 靠!

使用道具 举报

发表于 2026-3-13 08:45:14 | 显示全部楼层
tangptr@126.com 发表于 2026-3-13 00:46
[md]
Rust还没有这样的intrinsics,只能用内联汇编了。好在包装一下也不难,我在帖子里也说了,Rust内联 ...

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

使用道具 举报

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

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

回复 赞! 靠!

使用道具 举报

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

原来如此,那如果是%1 %2 这种用到的数量超过了寄存器上限了呢?
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2026-4-17 12:35 , Processed in 0.033520 second(s), 23 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表