有一天,我在研究某些古董代码时发现,由于Thompson Shell没有使用shebang行,所以可以将Windows PE文件编码为UNIX第六版的shell脚本。当我意识到有可能创建在Unix、Windows和MacOS平台上通用的二进制格式时,我就再也无法抵挡将其变为现实的诱惑了,因为这意味着高性能的原生代码几乎可以像Web应用程序一样跨平台使用。下面是它的工作原理。
MZqFpD=' BIOS BOOT SECTOR' exec 7<> $(command -v $0) printf '\177ELF...LINKER-ENCODED-FREEBSD-HEADER' >&7 exec "$0" "$@" exec qemu-x86_64 "$0" "$@" exit 1 REAL MODE... ELF SEGMENTS... OPENBSD NOTE... NETBSD NOTE... MACHO HEADERS... CODE AND DATA... ZIP DIRECTORY...
于是,我启动了一个名为Cosmopolitan的项目,用于实现Actually Portable Executable(真正可移植的可执行文件)格式。我之所以选择这个名字,是因为我喜欢这样的想法,即可以自由地编写超越传统界限的软件。我的目标是帮助C语言成为一种“一次构建,到处运行”的语言,并且要适合新手开发,同时避免任何可能阻止软件在技术社区之间共享的障碍。下面是入门的简单方法:
gcc -g -O -static -fno-pie -no-pie -mno-red-zone -nostdlib -nostdinc -o hello.com hello.c \ -Wl,--oformat=binary -Wl,--gc-sections -Wl,-z,max-page-size=0x1000 -fuse-ld=bfd \ -Wl,-T,ape.lds -include cosmopolitan.h crt.o ape.o cosmopolitan.a
上面的命令的作用,就是重新配置了Linux上的stock编译器,以使其输出可以在MacOS、Windows、FreeBSD、OpenBSD和NetBSD系统上运行的二进制文件。它们也从BIOS进行引导。请注意,这是为那些不关心桌面GUI,只想要stdio和sockets而不需要devops的人而准备的。
与平台无关的C/C++/FORTRAN工具
有谁能够料到跨平台的原生构建竟然会如此轻松呢?事实证明,它们的尺寸也出奇地小。即使加上所有魔数、win32 utf-8 polyfills和bios引导加载程序代码,得到的EXE文件最终仍然比Go Hello World小100倍:
· life.com 大小为12kb (symbols, source)
· hello.com 大小为16kb (symbols, source)
需要注意的是,zsh与Thompson Shell在向后兼容性方面存在一个小问题[2021-02-15更新:zsh现在已修复了该问题],所以运行时请使用sh hello.com而不是./hello.com命令。读者可能会有疑问:如果这件事情这么简单,为什么以前没有人这么做呢?据我所知的最佳答案是,这需要对ABI稍微进行某些修改,即将与系统接口有关的C预处理器宏用符号来表示。当然,这种修改并没有多大的难度,但是像switch(errno){case EINVAL:...}这样的情况除外。只要我们愿意修改某些规则,就可以将GNU Linker配置为在链接时生成我们需要的所有PE/Darwin数据结构,而无需借助于其他工具链。
PKZIP可执行文件是个不错的容器
实际上,由单文件组成的可执行文件是最容易处理的。在某些情况下,静态可执行文件通常依赖于某些系统文件,例如zoneinfo。然而,如果我们希望构建的二进制文件能够在多个发行版上运行,并且支持Windows系统的话,就不允许出现这种情况了。
事实证明,PKZIP被设计成将其魔法标记放在文件的结尾,而不是开头,所以我们也可以用ZIP合成ELF/PE/MachO二进制文件。我能够使用几行链接程序脚本以及一个用于增量压缩段的程序,在Cosmopolitan代码库中有效地实现该功能。
我们可以运行unzip -vl executable.com命令来查看其内容。此外,在Windows 10上也可以将文件扩展名改为.zip,然后在微软捆绑的ZIP GUI中打开它。有了这种能够在编译后轻松编辑相关资源的灵活性,意味着我们还可以做一些事情,比如创建一个易于分发的JavaScript解释器,通过zip反射加载经过解释的源代码。
· hellojs.com大小为300kb (symbols, source)
Cosmopolitan还使用ZIP格式来自动遵守GPLv2许可[更新2020-12-28:APE现在获得ISC许可]。在默认情况下,非商业的libre构建被配置为嵌入hermetic make mono repo中链接的所有源文件,从而导致二进制文件大约要大10倍。例如:
· life2.com大小为216kb (symbols, source)
· hello2.com大小为256kb (symbols, source)
摇滚乐手对动态范围压缩来说是相爱相杀的关系:一方面,它消除了音乐中一个复杂的维度;另一方面,为了听起来专业,它又是不可或缺的。Bloat可能也是按照同样的原理工作的。就这里来说,zip源文件嵌入可能是一种更有社会意识的浪费资源的方式,以获得对非经典软件消费者的吸引力。
x86-64 Linux ABI是一种非常好的通用语言
在计算史上,知道最近硬件架构才发生了明显的变化,TOP500榜单就是最好的证明。在手机、路由器、大型机和汽车之外,围绕x86的共识是如此强烈,以至于我习惯于将其比作巴别塔。多亏了Linus Torvalds,我们不仅在架构上达成了共识,而且在输入输出机制上也接近于达成共识,即让程序与主机通过SYSCALL指令进行通信。他通过坐在家里穿着浴袍给大公司发邮件,让他们同意把资源投入到创造与公有制悲剧截然相反的美丽的东西上,来实现这个目标。
因此,我觉得现在真的是对系统工程持乐观态度的最好时代。我们比以往任何时候都更同意分享共同的东西。但是,仍然有一些异类,比如我们在新闻中听到的苹果和微软的某些计划——他们试图让PC投入ARM的怀抱。我不知道为什么我们需要一个C级Macintosh,因为x86_64专利应该在今年到期。苹果也许可以不用支付专利费就能独立制造x86芯片。我们一直梦想的自由/开放架构,可能会变成我们一直在使用的架构。
如果微处理器体系结构最终将会达成共识,那么我认为我们应该专注于构建更好的工具,帮助软件开发人员从中受益。我一直致力于有望在这一领域做出贡献的方法之一,就是构建一种更友好的方式来可视化x86-64执行对内存的影响。它应该有助于帮助读者理解真正可移植的可执行文件的工作原理。(演示视频请参见原文)
您将注意到,开始执行时会将Windows PE头当作代码来处理的。例如,ASCII字符串“mzqfpd”被解码为pop %r10 ; jno 0x4a ; jo 0x4a,而字符串“\177ELF”则解码为jg 0x47。然后,它会跳过一个mov语句,告诉我们程序是从用户空间运行的,而不是引导的,然后跳到入口点。
然后,使用分散的部分和GNU Assembler.sleb128指令,可以轻松地为主机操作系统解码出魔数。像UNICODE位查找表这样的低熵数据通常使用103字节的LZ4解压缩器或17字节的运行长度解码器进行解码,而使用Intel大小为3KB的x86解码器可以很容易地实现运行时代码变形。
请注意,这个模拟器并不是必需的。对于真正可移植的可执行文件(Actually Portable Executable)来说,只要在shell、NT命令提示符下运行,或者从BIOS引导,就可以很好地工作。注意,这并非JVM,只有在需要时才使用模拟器。例如,对于我们来说,对程序执行如何影响内存进行可视化是很有帮助的。
如果我们编写的普通PC程序都可以在Raspberry Pi和Apple ARM上“正常工作”的话,将是一件非常让人高兴的事情。我们要做的所有工作,都是将上述仿真器的ARM构建嵌入到我们的x86可执行文件中,对其进行适当的修改并重新执行,这类似于Cosmopolitan已经对qemu-x86_64进行的操作,只是不需要预先安装。这么做的代价是,我们的二进制文件将仅比Go语言的Hello World程序小10倍,而不是小100倍。另一个代价是,GCC运行时异常会禁止代码变形,但是我已经通过重写GNU运行时解决了这一问题。
将x86-64-linux-gnu做得尽可能小的最有说服力的用例是,在完全仿真的情况下,它可以使普通的简单本地程序在任何地方运行,包括默认的Web浏览器。在这方面的解决方案具有许多不尽如人意的地方,要么过于关注还没有达成共识的接口,比如GUI和线程,要么只是模拟整个操作系统,就像Docker或者Fabrice Bellard在浏览器中运行Windows一样。我认为,我们需要的兼容性胶水只需运行程序,而应该忽略系统,把x86_64-linux-gnu当作一个规范的软件进行编码。
使用寿命长,无需维护
我喜欢使用这些老掉牙、毫无吸引力的技术的原因之一是,我希望我所从事的任何软件工作都能以最少的努力经受住时间的考验,比如类似于Super Mario Bros的ROM在不需要GitHub问题跟踪器的情况下成功生存了这么多年的情形。
我相信,实现上述目的的最佳方案是将已经达成数十年共识的二进制接口粘合在一起,而忽略API。例如,以下是Mac、Linux、BSD和Windows发行版使用的魔数。大家不妨仔细看一眼,因为这些数字为您使用的几乎所有计算机、服务器和电话的内部结构提供了支撑。
如果我们关注所有系统共有的魔数子集,并将其与它们的共同祖先Bell System Five进行比较,我们就会发现在过去的40年中,在二进制级别上,关于系统工程的变化实际上非常少见。平台不可能在不破坏它们自身的情况下破坏它们。多年来,很少有人提出关于为什么UNIX哲学需要改变的观点。
下载地址 [Linux] [FreeBSD] [OpenBSD] [DOS] [Windows] [MacOS]
· emulator.com (280k PE+ELF+MachO+ZIP+SH)
· tinyemu.com (188k PE+ELF+MachO+ZIP+SH)
源代码下载地址
· ape.S
· ape.lds
相关程序
· life.com (12kb ape symbols)
· sha256.elf (3kb x86_64-linux-gnu)
· hello.bin (55b x86_64-linux-gnu)
示例
· bash hello.com # runs it natively
· ./hello.com # runs it natively
· ./tinyemu.com hello.com # just runs program
· ./emulator.com -t life.com # show debugger gui
· echo hello | ./emulator.com sha256.elf
使用说明
SYNOPSIS
./emulator.com [-?HhrRstv] [ROM] [ARGS...] DESCRIPTION Emulates x86 Linux Programs w/ Dense Machine State Visualization Please keep still and only watchen astaunished das blinkenlights
FLAGS
-h help -z zoom -v verbosity -r real mode -s statistics -H disable highlight -t tui debugger mode -R reactive tui mode -b ADDR push a breakpoint -L PATH log file location
参数
ROM文件可以是ELF文件或一个真正可移植的可执行文件(Actually Portable Executable)。
按照System Five ABI的要求,它应该使用x86_64。
系统调用ABI是在Linux内核中定义的。
特性
8086, 8087, i386, x86_64, SSE3, SSSE3, POPCNT, MDA, CGA, TTY
网站
https://justine.lol/blinkenlights/
参考资料
还没有评论,来说两句吧...