让我们以一个非常简单的程序为例,一个什么都不做的程序 将数字返回给操作系统。为什么不呢?毕竟,Unix 已经附带了不少于两个这样的程序:true 和 假。由于已经取了 0 和 1,我们将使用数字 42。
所以,这是我们的第一个版本:
/* tiny.c */int main(void) { return 42; }
我们可以像这样编译和测试:
$ gcc -Wall tiny.c$ ./a.out ; echo $?//返回返回值42
图示操作如下:
所以。它有多大?它有多大,好吧,在我的机器上,我得到:
$ wc -c a.out3998 a.out
(你的可能会有所不同。诚然,这是相当小的 今天的标准,但几乎可以肯定比它需要的要大。
我的机器上结果如下:
显而易见的第一步是剥离可执行文件:
这当然是一个进步。对于下一步,怎么样 优化?
这在理论上也有所帮助,但实际上:几乎没有任何需要优化的东西。
我们似乎不太可能做太多其他事情来缩小 单语句 C 程序。我们将不得不把 C 抛在后面,然后 请改用汇编程序。希望这将减少所有额外的内容 C 程序自动产生的开销。
所以,进入我们的第二个版本。我们需要做的就是从 main() 中。在汇编语言中,这意味着函数应设置 累加器 EAX 设置为 42,然后返回:
首先建立.asm文件
gedit tiny.asm
内容如下:
; tiny.asmBITS 32GLOBAL mainSECTION .textmain:mov eax, 42ret
编译指令如下:
$ nasm -f elf tiny.asm$ gcc -Wall -m32 -s tiny.o$ ./a.out ; echo $?42
结果如下:
这里可以注意,编译的时候报了一个错误:
问题原因:
在64位系统下去编译32位的目标文件,这样是非法的。解决方案:
用”-m32”强制用32位ABI去编译,即可编译通过。
查看当前大小如下:从14328到13656减少了672.
好吧,问题是我们仍然会产生大量的开销 使用 main() 接口。链接器仍在添加一个接口 我们的操作系统,正是该接口实际上调用了 main()。所以 如果我们不需要它,我们如何解决这个问题?
默认情况下,链接器使用的实际入口点是符号 名称为 _start。当我们与gcc链接时,它会自动 包括一个 _start 例程,一个设置 argc 和 argv 的例程,以及其他 things,然后调用 main()。
所以,让我们看看我们是否可以绕过它,并定义我们自己的_start 常规
; tiny.asmBITS 32GLOBAL _startSECTION .text_start:mov eax, 42ret
gcc 会做我们想做的事吗?
不。好吧,实际上,是的,它会的,但首先我们需要学习如何提问 为了我们想要的。
碰巧 gcc 识别一个名为 -nostartfiles 的选项。 从 gcc 信息页面:
-nostartfiles
在以下情况下不要使用标准系统启动文件 连接。标准库正常使用。
啊哈!现在让我们看看我们能做些什么:
好吧,gcc 没有抱怨,但该程序不起作用。发生了什么 错?
问题在于我们把_start当作一个 C 函数来对待, 并试图从中返回。实际上,它根本不是一个功能。 它只是链接器用来定位的目标文件中的一个符号 程序的切入点。当我们的程序被调用时,它就会被调用 径直。如果我们看一下,我们会看到顶部的值 堆栈中的数字是 1,这当然非常 un-address-like。事实上,堆栈上的是我们程序的 argc 价值。在此之后是 argv 数组的元素,包括 终止 NULL 元素,后跟 envp 的元素。这就是 都。堆栈上没有返回地址。
那么,_start是如何退出的呢?好吧,它调用了 exit() 函数! 毕竟,这就是它的用途。
实际上,我撒谎了。它真正做的是调用 _exit() 函数。 (请注意前导下划线。exit() 是完成一些 代表流程的任务,但这些任务永远不会 started,因为我们绕过了库的启动代码。所以我们 还需要绕过库的关机代码,直接转到 操作系统的关机处理。
所以,让我们再试一次。我们将调用 _exit(),它是一个 函数,该函数采用单个整数参数。因此,我们需要做的就是 将数字推送到堆栈上并调用该函数。(我们还需要 将 _exit() 声明为外部。这是我们的组装:
; tiny.asmBITS 32EXTERN _exitGLOBAL _startSECTION .text_start:push dword 42call _exit
查看现在的结果:减少了588字节,变成了13068
嗯......那么还有什么其他的 GCC 有有趣的晦涩选项吗?
好吧,这个,紧跟在 -nostartfiles 之后 文档,当然是引人注目的:
-nostdlib
不要使用标准的系统库和启动链接时的文件。只有您指定的文件才会 传递给链接器。
这值得研究:
哎呀。没错...... 毕竟,_exit() 是一个库函数。 它必须从某个地方填写。
好。但可以肯定的是,我们不需要 libc 的帮助来结束一个程序, 我们呢?
不,我们没有。如果我们愿意抛开所有伪装 可移植性,我们可以使我们的程序退出而不必链接 别的东西。不过,首先,我们需要知道如何制作一个系统 在 Linux 下调用
与大多数操作系统一样,Linux 为 它通过系统调用托管的程序。这包括打开等内容 一个文件,读取和写入文件句柄 - 当然,还有 关闭进程。
Linux 系统调用接口是一条指令:int 0x80。 所有系统调用都通过此中断完成。要进行系统调用, EAX 应包含一个数字,用于指示正在进行哪个系统调用 调用,其他寄存器用于保存参数(如果有)。 如果系统调用采用一个参数,则该参数将位于 ebx 中;一个系统 具有两个参数的调用将使用 EBX 和 ECX。同样,edx、esi 和 如果需要第三个、第四个或第五个参数,则使用 EDI, 分别。从系统调用返回后,eax 将包含 返回值。如果发生错误,eax 将包含一个负值, 绝对值表示错误。
不同系统调用的号码列在 /usr/include/asm/unistd.h。快速浏览一下就会告诉我们,出口 系统调用被分配为数字 1。像 C 函数一样,它需要 一个参数,返回到父进程的值,等等 将进入 EBX。
我们现在知道了创建下一个版本所需的所有信息。 程序,一个不需要任何外部函数帮助的程序 工作
; tiny.asmBITS 32GLOBAL _startSECTION .text_start:mov eax, 1mov ebx, 42 int 0x80
查看结果,从减少了288字节,变成了12780
所以。。。我们还能做些什么来让它变得更小吗?
使用更短的指令怎么样?
如果我们为汇编代码生成一个列表文件,我们将找到 以后:
00000000 B801000000 mov eax, 100000005 BB2A000000 mov ebx, 420000000A CD80 int 0x80
好吧,哎呀,我们不需要初始化所有 ebx,因为操作 系统将只使用最低的字节。单独设置 bl 将是 足够了,并且将占用两个字节而不是五个字节。
我们还可以通过将 eax 设置为 1 来将其 xor'ing 为零,然后使用 单字节增量指令;这将节省两个字节。
00000000 31C0 xor eax, eax00000002 40 inc eax00000003 B32A mov bl, 4200000005 CD80 int 0x80
我认为可以肯定地说,我们不会这样做 编程任何比这更小的程序。
顺便说一句,我们不妨停止使用 gcc 来链接我们的可执行文件, 鉴于我们没有使用它的任何附加功能,而只是 调用链接器 LD,我们自己:
关于为 Linux 创建真正 Teensy ELF 可执行文件的旋风教程 (muppetlabs.com)
后续的内容都是按着这篇文章写得,实现之后实在懒得写博客了。。。。
最后的实现压缩成45字节