创建Linux下可运行的超小型ELF可执行文件(1)

p { margin-bottom: 0.21cm; 

如果你是一个很自大的程序员,那么你可能发现本文是完美的灵丹妙药。

 

这篇文档探索用于减少简单程序中过量的字节数。当然,本文的更实用的目的是描述 ELF 文件格式和 Linux 操作系统的一些内部工作原理。但是在这个过程中你很有希望也学会怎么制作超小型 ELF 可执行文件。

 

请注意这里给出的信息和示例绝大部分是针对运行的 intel-386 架构上的 Linux 平台的 ELF 可执行文件。我认为这里的很多信息同样适用于其他基于 ELF UNIX 变体,但是我这方面的经验确实有限,所以不敢保证确实如此。

 

另外,如果一点都不懂汇编代码,你可能会觉得本文的一些部分有点难懂。本文中使用的汇编大马是 Nasm 风格的,参考 http://www.nasm.us/

 

在开始探索之前,我们需要一个程序。几乎任何程序都行,但是我们对于到底能将应用程序变得有多小而不是这个程序有什么功用感兴趣,所以它应该越简单越好。

 

接下来我们来做一个非常简单的程序,它仅仅返回一个数字给操作系统。

这是该程序的初始版:

/* tiny.c */

int main(void) { return 42; }

编译命令:

$ gcc -Wall tiny.c

$ ./a.out ; echo $?

42

 

那么编译后产生的可执行文件有多大呢?,在我自己的机器上如下:

$ wc -c a.out

3998 a.out

 

第一步当然是用 strip 命令参数来对可执行我嫩见瘦身:

$ gcc -Wall -s tiny.c

$ ./a.out ; echo $?

42

$ wc -c a.out

2632 a.out

明显有所提高。下一步,试试代码优化如何?

$ gcc -Wall -s -03 tiny.c

$ wc -c a.out

2616 a.out

这也有所帮助,但是效果一般般。不难理解:程序里确实没有什么可以优化的。

 

看起来对于压缩一个只有一行语句的 C 程序的可执行文件大小似乎没什么可以做的了。我们将不得不扔下 C ,使用汇编代码来代替。这很有希望除掉 C 程序自动造成的额外空间消耗。

 

所以,在我们的第二版中,我们将返回值 42 放在 eax 寄存器中然后返回:

; tiny.asm

BITS 32

GLOBAL main

SECTION .text

main:

mov eax, 42

ret

编译命令如下:

$ nasm -f elf tiny.asm

$ gcc -Wall -s tiny.o

$ ./a.out ; echo $?

42

那它生成的可执行文件有多大呢?

$ wc -c a.out

2604 a.out

 

看样子我们只减掉了可怜的 12 字节。

 

嗯,关键还在于我们使用了 main() 接口并因此导致大量的额外空间消耗。连接器仍然会为我们往操作系统中添加一个接口,就是这个接口真正调用 main() 。那么,如果不用这个接口,我们怎样来绕开呢?

 

连接器默认使用的真实入口点是符号 _start 。当我们用 gcc 链接时,它自动包含例程 _start ,这个例程会建立传入参数 argc argv ,还做一些其他事情,然后调用 main()

 

我们试试定义自己的例程 _start ,看能否绕开这点:

; tiny.asm

BITS 32

GLOBAL _start

SECTION .text

_start:

mov eax, 42

ret

编译一下试试:

$ nasm -f elf tiny.asm

$ gcc -Wall -s tiny.o

tiny.o(.text+0x0): multiple definition of '_start'

/usr/lib/crt1.o(.text+0x0): first defined here

/usr/lib/crt1.o(.text+0x36): undefined reference to `main'

编译错误!不过,确实可以这样做,但是首先哦我们需要学会请求我们需要什么。

gcc 识别一个编译选项 -nostartfiles 。注释如下:

-nostartfiles

在链接时不使用标准的系统 startup 文件。标准库仍然正常使用。

 

现在我们再试试:

$ nasm -f elf tiny.asm

$ gcc -Wall -s -nostartfiles tiny.o

$ ./a.out ; echo $?

Segmentation fault

139

嗯, gcc 编译是通过了,但是程序运行失败。哪个地方有问题?

 

原来我们把 _start 当成了 C 函数,并试图从它返回。实际上,它并不是一个函数,仅仅是一个目标文件中的符号而已。连接器只是用它来定位程序的入口点。当我们查看应用程序的堆栈时,会发现栈顶是数字 1 ,这显然不像是一个内存地址。事实上,这是 argc 的值,接下来是 argv 数组,包含结束元素 NULL ,然后是 envp 。并没有发现返回地址。

 

那么 _start 是怎样退出的呢?,是通过调用 exit() 函数!

更确切的说是调用 _exit() 函数。 exit() 会结束进程中的一些其他任务,但是这些任务还没有被启动,因为我们绕过了类库的 startup 代码。

 

接下来再试试 _exit() 看:

; tiny.asm

BITS 32

EXTERN _exit

GLOBAL _start

SECTION .text

_start:

push dword 42

call _exit

用之前的命令编译一下:

$ nasm -f elf tiny.asm

$ gcc -Wall -s -nostartfiles tiny.o

$ ./a.out ; echo $?

42

终于成功了!看看有多大:

$ wc -c a.out

1340 a.out

几乎让可执行文件的大小减半了!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值