最近,我再次尝试在Linux和Windows上编写除了操作系统本身之外没有aruntime的软件。另一种看待它的方式是:我在应用程序中编写并嵌入一个定制的、最低限度的运行时。运行时的核心任务之一是从操作系统中检索命令行参数。在窗户上,这是一个比我想象的更深的兔子洞,比我想象的要复杂得多。没有标准,每个运行时都有一些不同。五个不同的应用程序可能会从同一个输入中看到五组不同的参数,甚至是不同的参数计数,这是在任何形式的选项解析之前。这真的是一座现代的贝尔大厦:“混淆他们的命令行解析,他们可能不理解别人的论点。”
类Unix系统将argv数组直接从父级传递到子级。OnLinux它实际上是复制到孩子的堆栈上,就在条目上的堆栈指针上方。运行时只需将堆栈指针地址增加一个字节,并将其称为argv。下面是一个最简单的x86-64 Linux运行时injust 6指令(22字节):
_开始:移动电子数据交换[rsp];argc-lea-rsi[rsp+8];argv呼叫主mov edi,eax mov eax,60;退出系统调用
_开始:ldr w0,[sp];argc加x1,sp,8;argv bl main mov w8,93;系统退出svc 0
在Windows上,argv以序列化形式作为字符串传递。这就是MS DOS的做法(通过程序段前缀),因为CP/M就是这样做的。当进程主要由人类直接启动时,它就更有意义了:字符串实际上是由人类操作符键入的,毕竟必须有人对其进行解析。如今,进程几乎总是由其他程序启动的,但尽管如此,仍必须将参数数组序列化为字符串,就像是人工输入的一样。
Windows本身提供了一个用于解析命令行字符串的操作系统例程:CommandLineToArgvW。使用GetCommandLineW获取命令行Strings,并将其传递给此函数,您就有了argc和argv。再加上也许可以免费清理。它只以“宽”形式提供,所以如果你想在UTF-8中工作,你还需要宽图表多字节。它大约是20行C而不是6行组件,但也不算太糟。
GetCommandLineW将指针返回到静态存储中,这就是它不需要被释放的原因。更具体地说,它来自ProcessEnvironment块。这让我思考:我能在没有API调用的情况下自己定位这个地址吗?首先我需要找到皮布。经过一些研究,我在线程信息块中发现了一个PEB指针,它本身是通过gs寄存器(x64,x86上的fs)找到的,这是一个旧的386段寄存器。PEB中隐藏着一个UNICODE_字符串,带有命令行字符串地址。我计算了x86和x64的所有偏移量,整个过程只有三条指令:
(3)如果(4)有人在(和#;34;mov%%%%gs:(0x60,,%0\n和(34)0\n和(34)0;mov%%:(0x60,,,%0\0\n;mov%%:(0x60,,%0\n和(34)n和(34)34;34;34;mov 0x20(0,,,,,%0\0\n\n和(34;34;34;34;34;34;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;3;0;0;0;0;0;0;0;0;0;0;0;0;0;3;0;0;0;0;3;0;3;3;0;3;3;3;34;34;34;34;3;和\35;34;34;34;34;34;和;%0\n";";mov 0x10(%0),%0\n";";mov 0x44(%0),%0\n";:";=r";(cmd))#endif return cmd;}
从Windows XP到Windows 11,这将返回与GetCommandLineW完全相同的地址。除了陈雷蒙(Raymond Chen)之外,没有什么理由这么做,但它仍然很整洁,可能还有一些超级尼切(nicheuse)。从技术上讲,这些偏移量中的一些是未记录的和/或可能发生变化的,除了微软自己的静态链接CRT还硬编码所有这些偏移量。很容易找到:反汇编任何静态链接的程序,查找gs寄存器,也可以使用这些偏移量找到它。
如果你仔细观察UNICODE_字符串,你会发现长度是以字节为单位的USHORT给出的,尽管它是一个16位的wchar_字符串。这是Windows命令行最大长度32767个字符(包括终止符)的来源。
GetCommandLineW来自内核32。dll,但CommandLineToArgvW在shell32中有点与众不同。dll。如果你不想和shell32联系。不管出于什么原因,您都需要自己执行命令行解析。许多运行时,包括微软自己的CRT,都不调用命令行,而是自己进行解析。它比我预想的更混乱,当我开始深入研究它时,我没想到它会进行几天的研究。
GetCommandLineW有一个粗略的解释:在空白处拆分参数(未定义),涉及引用,还有一些关于倒斜杠计数的内容,但前提是它们在引用处停止。仅仅实现自己的文档是不够的,如果你用它进行测试,很快就会发现这个文档充其量是不完整的。它将链接到一个关于如何解析C++命令行参数的弃权页面。不幸的是,本页描述的算法不是GetCommandLineW使用的算法,也不是我能找到的任何运行时使用的算法。Iteven因微软自己的CRT而异。没有规范的命令行解析结果,甚至没有事实上的标准。
我最终看到了David Deley的《如何解析命令行参数》(How Command Line Parameters Ares Parsed),这是关于这个问题的最接近权威文档(也是)。不幸的是,它关注的是运行时,而不是ArgVw的命令行,因此其中一些细节没有被捕获。特别是,第一个参数(即argv[0])遵循完全不同的规则,这让我一时困惑。葡萄酒文档对ArgVW的命令行尤其有用。据我所知,他们已经完美地实现了它,一个错误一个错误地匹配它。
在找到这些之前,我开始构建自己的实现,现在我相信它与CommandLine和ArgVW相匹配。这些其他文件帮助我找出我遗漏了什么。以我通常的方式,它是一个小小的状态机:cmdline。c、 界面:
与其他机型不同,我的机型直接编码到WTF-8,这是一个UTF-8的超集,可以往返于格式不正确的UTF-16。WTF-8部分是代码的反面:不可见,因为它不涉及对格式错误的输入做出反应。如果使用新的ish UTF-8 manifest Win32功能,则程序无法处理格式错误的UTF-16命令行字符串,WTF-8解决了这个问题。
如文件所述,argv必须是一个特定的大小——一个指针对齐的224kB(x64)或160kB(x86)缓冲区——这涵盖了绝对最坏的情况。当命令行被限制为32766个UTF-16字符时,这并不太糟糕。最坏情况下的参数是一个3字节的长序列。4字节UTF-8需要2个UTF-16代码点,因此只能有相同数量的UTF-8。最糟糕的情况是argc是16383(加上一个argv slotf作为空指针终止符),这是每对命令行字符的一个参数。argv的后半部分(大致)实际上用作参数的字符缓冲区,因此它是一个单一的固定分配。没有错误案例,因为它不会失败。
另外:请注意我的源代码中的FUZZ选项。它经过了相当彻底的模糊测试。它什么也没发现,但它确实让我对结果更有信心。
我还查看了一些语言运行时,看看其他语言如何处理它。正如所料,Mingw-w64具有旧的(2008年之前)MicrosoftCRT的性能。同样,CPython隐式地执行底层Cruntime所做的任何操作,因此其确切的命令行行为取决于使用哪个版本的Visual Studio来构建Python二进制文件。OpenJD和Rust(LLVM)都实用主义地将命令行称为argvw。Go(gc)自己进行解析,其行为在CommandLineToArgVWw和一些微软的CRT之间混合,但两者都不太匹配。
我一直很困惑,为什么对argvw来说,commandlines没有互补的反义词。当生成带有任意参数的进程时,每个人都需要在指定的非平凡命令行格式下实现与此相反的操作,以序列化argv。希望接收器能兼容地解析它!没有人会求助于系统程序来提供帮助。这导致了大量的重复工作:它不局限于高级运行时,而是几乎所有可扩展的应用程序(本身类似于运行时)。幸运的是,序列化并不像Asposing那么复杂,因为如果以一种前瞻性的方式进行,许多边缘案例根本不会出现。
与之前一样,它接受WTF-8 argv,这意味着它可以正确地传递格式错误的UTF-16参数。它返回实际的命令行长度。因为当argv太大时,这个函数可能会失败,所以它会返回szero以获取一个错误。
char*argv[]={";python.exe";,";-c";,代码,0};wchar_t cmd[CMDLINE_cmd_MAX];如果(!cmdline_from_argv8(cmd,cmdline_cmd_MAX,argv)){return";argv过大";}如果(!CreateProcessW(0,cmd,/*…*/){return";CreateProcessW failed";;}
陈旧的Emacs实现是用C语言编写的,而不是用Lisp语言编写的,它沉浸在历史的长河中,有着残存的错误转折。Emacs仍然只调用“狭窄的”CreateProcessA,尽管它对dootherwise有各种启示,但它使用了错误的编码。头痛的根源。
CPython通过子流程使用Python而不是C。list2cmdline。虽然没有文档记录,但它可以在任何平台上访问,并且很容易对各种输入进行测试。试试看!
OpenJDK乐观地优化了80字节以下的命令行字符串,并且像Emacs一样,显示了长期使用的耐候性。
我不打算在短期内编写语言实现,因为可能需要这样做,但很高兴知道我已经为自己解决了这个问题!
你对这篇文章有何评论?通过向~skeeto/public发送电子邮件,在我的公共收件箱中开始讨论[email protected][邮件列表礼仪],或查看现有讨论。