原文地址:nullprogram.com/blog/2021/0…
原文作者:nullprogram.com/
发布时间:2021年2月8日
自从w64devkit成立以来,我就面临着两难的境地,它是我在Windows上的一体化的Mingw-w64工具链和开发环境发行版。这个项目的一个主要目标是不需要安装:在任何地方解压,就可以直接使用。然而,完整的功能需要别名命令,特别是对于BusyBox小程序,通常的解决方案既不可用也不可行。看来需要一个安装程序来组装这最后一块拼图。上周末,我终于发现了一个整齐完整的解决方案,彻底解决了这个问题。
这个解决方案是一个小的C源文件,alias.c。本文介绍的是为什么需要它,以及它是如何工作的。
硬链接和符号链接
有些别名命令是为了方便,比如gcc的cc别名,这样构建系统就不需要使用任何特定的C编译器。另一些则是必不可少的,比如 "busybox sh "的sh别名,这样它就可以作为make的shell。这些别名通常是通过硬链接或符号链接创建的。一个GCC的安装可能包括(大致)一个符号链接,就像这样创建。
ln -s gcc cc
BusyBox 在启动时查看它的 argv[0],如果它命名了一个 applet (ls, sh, awk 等),它的行为就像那个 applet。一般来说,BusyBox的别名都是作为原始二进制文件的硬链接安装的,甚至有一个busybox --install来设置这些别名。这两种别名都很便宜,也很有效。
ln busybox sh
ln busybox ls
ln busybox awk
不幸的是,Windows上的.zip文件不支持链接。它们需要由一个专门的安装程序来创建。因此,我强烈建议用户在某些时候运行 "busybox --install "来建立BusyBox别名命令。虽然w64devkit在没有它们的情况下也能工作,但有了它们就会更好。不过,这也是一个安装步骤。
另一种选择是为每个小程序(全部150个)简单地包含一个完整的BusyBox二进制副本,模拟硬链接。BusyBox很小,平均每个小程序约4kB,但也不是很小。由于.zip格式不使用块压缩--文件是单独压缩的--这种重复会出现在.zip本身。我的573kB的BusyBox构建复制了150次,会使发行版的大小增加一倍,安装足迹增加25%。这是不值得的。
既然.zip如此有限,也许我应该使用另一种支持链接的分发格式。然而,w64devkit的另一个目标是不假设安装了什么其他工具。Windows原生支持.zip,即使这种支持不是那么好(性能差、可合成性低、功能缺失等)。只需在一个全新的、离线的Windows安装上安装w64devkit .zip,您就可以在一分钟内开始高效地开发专业的本地应用程序。
脚本作为别名
在链接不存在的情况下,下一个最好的选择是shell脚本。在类似unix的系统中,shell脚本是创建复杂别名命令的有效工具。与链接不同的是,它们可以操纵参数列表。例如,w64devkit包含一个c99别名,用于调用配置为使用C99标准的C编译器。要通过 shell 脚本来实现这一目的,可以使用
#!/bin/sh
exec cc -std=c99 "$@"
这将在参数列表中加入 -std=c99,并通过 Bourne shell 的特殊情况"$@"来传递其余的参数。因为我使用了exec,所以shell进程就成了原地的编译器。shell并没有在后台闲逛。它只是消失了。这真的相当优雅和强大。
Windows上最接近的是一个.bat批处理文件。然而,就像DOS和Windows的其他一些部分一样,Batch语言的设计就像它的设计者曾经瞥见有人在使用unix shell,也许在他们的肩膀上看了一眼,然后在不理解的情况下复制了一些想法。因此,它的实用性和强大性都不高。下面是相当于Batch的。
@cc -std=c99 %*...
@是必要的,因为Batch默认打印它的命令(Bourne shell的-x选项),而@则禁用它。Windows缺乏exec(3)的概念,所以Batch文件解释器cmd.exe继续和编译器一起运行。有点浪费,但这几乎不重要。不过,重要的是 cmd.exe 并没有表现出自己的特性。如果你,比如说,按Ctrl+C取消编译,你会得到臭名昭著的 "终止批处理工作(Y/N)?"的提示,这会干扰在同一控制台运行的其他程序。所谓的 "批处理 "脚本根本就不是批处理作业:它是交互式的。
我曾试过使用Batch文件来处理BusyBox小程序,但这个问题不断出现,使得这种方法不切实际。几乎所有的BusyBox小程序都是非交互式的,当它们不是交互式的时候,很多东西都会损坏。最糟糕的是,你可以很容易地以层层cmd.exe互相攻击来询问它们是否应该终止而告终。这很令人沮丧。
提示符是在cmd.exe中硬编码的,不能被禁用。由于很大程度上依赖于cmd.exe保持完全的方式,微软也不会改变这种行为。毕竟,这也是他们把PowerShell做成一个新的、独立的工具的原因。
说到PowerShell,我们可以用它来代替吗?可惜不能。
它在Windows系统中是默认安装的,但不一定能启用。我自己使用w64devkit的一个案例涉及到PowerShell被策略禁用的系统。一个常见的策略是它可以被交互式地使用,但不能运行脚本("本系统禁止运行脚本")。
PowerShell在Windows上不是一等公民,而且很可能永远不会是。即使在最友好的政策下,通常也不可能将PowerShell脚本放在PATH上并通过名字运行。我相信有办法通过系统范围内的配置来实现这一目标,但这是不可能的)。
第(2)项也影响到w64devkit。它有一个Bourne shell,但shell脚本仍然不是一等公民,因为Windows不知道如何处理它们。修复需要在全系统范围内进行配置,这与项目的理念相悖。
解决方案:编译shell "脚本"
我的工作解决方案的灵感来自于我最喜欢的媒体播放器mpv所使用的一个非常聪明的黑客。乍一看,Windows的构建很奇怪,包含两个二进制文件,mpv.exe(大)和mpv.com(小)。那是老式的16位DOS二进制中的COM吗?不,这只是一个绕过Windows限制的技巧。
Windows技术被分成了多个子系统。控制台程序运行在控制台子系统中。图形程序运行在Windows子系统中。最初的WSL就是一个子系统。不幸的是,这种设计意味着程序必须静态地选择一个子系统,硬编码到二进制映像中。程序不能动态地选择一个子系统。例如,这就是为什么Java安装有java.exe和javaw.exe,Emacs有emacs.exe和runemacs.exe。不同的子系统有不同的二进制文件。
在Linux上,一个程序如果要做图形,只需要和Xorg服务器或者Wayland合成器对话。它可以动态地选择做一个终端应用或图形应用。甚至可以同时做两个。这正是mpv的行为,它在Windows上面临一个难题。有了子系统,它怎么能两者兼得呢?
诀窍是基于环境变量PATHEXT,它告诉Windows如何优先处理基名相同但文件扩展名不同的可执行文件。如果我输入mpv,它同时找到了mpv.exe和mpv.com,哪个二进制文件会运行?它将是PATHEXT中列出的第一个,默认情况下,它的开头是。
PATHEXT=.COM; .EXE; .BAT; ...
所以它将运行mpv.com,它实际上是一个普通的PE+.exe的伪装。Windows子系统mpv.exe获得快捷方式和文件关联,而控制台子系统mpv.com捕获命令行调用,并在调用真正的mpv.exe时作为控制台联络。巧妙!
我意识到我可以用类似的技巧来创建命令别名--不是.com的技巧,而是微型标志程序。如果我可以把每一个Batch文件编译成微小的、行为良好的.exe文件,这样它就不会依赖行为不良的cmd.exe了......
小C程序
几年前,我写过关于微小的、独立的Windows可执行文件的文章。这项研究在这里得到了回报,因为这正是我想要的。别名命令程序只需要操作它的命令行,调用另一个程序,然后等待它完成。这不需要C库,只需要调用少量的kernel32.dll。我的别名命令程序可以非常小,以至于我有150个别名命令程序已经不重要了,而且我可以完全控制它们的行为。
在我的旧文中,我提到了一个问题,GCC动态链接自己的堆栈探测函数。我已经在w64devkit中修正了这个问题,所以-nostdlib和-ffreestanding就足够了,再加上-lkernel32就可以把它拉回来。我仍然使用-Os(优化大小)和-s(条带)来使结果尽可能的小。
我不想为每个别名命令写一个小程序。相反,我将使用几个C定义,EXE和CMD,在编译时注入目标命令。所以这个Batch文件。
@target arg1 arg2 %*
相当于这个别名编译。
gcc -DEXE='L"target.exe"' -DCMD='L"target arg1 arg2"' \
-s -Os -nostdlib -ffreestanding -o alias.exe alias.c -lkernel32
EXE 字符串是实际的模块名称,所以需要使用 .exe 扩展名。CMD 字符串取代命令行的第一个完整标记 (比如 argv[0]),并且可以包含任意的附加参数 (比如 -std=c99)。两者都是宽字符串(L"..."),因为别名程序为了完全透明,使用了宽的Win32 API。虽然不幸的是目前没有区别。目前所有的别名程序都使用 "ANSI "API,因为底层的C和C++标准库只使用ANSI API。(据我所知,从来没有人为Windows编写过全功能的C和C++标准库,即使Microsoft也没有)
你可能会好奇为什么我要把字符串粘在一起作为参数。这些参数需要由其他人来解析(单词分割等),所以我不应该构造一个argv数组吗?Windows上不是这样的。程序会收到一个扁平的命令串,然后自己按照格式规范进行解析。当你写一个C程序时,C运行时会为你提供通常的argv数组。
这是倒过来的。创建进程的调用者已经将参数拆分为一个argv数组--或者类似的东西,但是Win32要求调用者按照特殊的格式将argv数组编码为一个字符串,这样接收者就可以立即对其进行解码。为什么首先要进行marshaling而不是传递结构化数据?为什么Win32只提供了一个解码器(CommandLineToArgv),而没有提供一个编码器(例如缺少的ArgvToCommandLine)?嘿,规则不是我定的,我只能忍受。
你可以看一下原始的源码以了解细节,但总结起来就是我提供了我自己的xstrlen()、xmemcpy()和部分Win32命令行解析器--足以识别第一个标记,即使这个标记是引号。它将这些字符串粘合在一起,调用CreateProcessW,等待它退出(WaitForSingleObject),检索退出代码(GetExitCodeProcess),并以同样的状态退出。(exec(3)免费附带的东西)。
这一切都被编译成一个4kB的可执行文件,主要是填充,对于我的目的来说已经足够小了。在.zip文件中,这些压缩到每一个可接受的1kB。再小一点就更好了,但这至少需要一个自定义的链接脚本,甚至更小的需要手工组装。
这个挥之不去的问题解决了,w64devkit现在比以前更好用了。alias.c源码包含在工具包中,以备你需要制作任何你自己的乖巧的别名命令。
通过www.DeepL.com/Translator(免费版)翻译