当前位置: 首页 >> 程序设计 >> 在linux平台上创建超小的ELF可执行文件
 

在linux平台上创建超小的ELF可执行文件

作者:      来源:     发表时间:2006-06-07     浏览次数:      字号:    

文章来源:http://www.xfocus.org
文章提交:if.q@qq.com
在linux平台上创建超小的ELF可执行文件

作者:breadbox <breadbox@muppetlabs.com>
原文<A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux >
整理翻译:alert7    <alert7@21cn.com>
主页:    http://www.xfocus.org/

--------------------------------------------------------------------------------


前言:
    有些时候,文件的大小是很重要的,从这片文章中,也探讨了ELF文件格式内部的工作
情况与LINUX的操作系统。该片文章向我们展示了如何构造一个超小的ELF可执行文件。

文章中给出的这些example都是运行在intel 386体系的LINUX上。其他系统体系上或许也有同样的
效果,但我不感肯定。

我们的汇编代码使用的是Nasm写的,它的风格类似于X86汇编风格。
NASM软件是免费的,可以从下面得到
http://www.web-sites.co.uk/nasm/



--------------------------------------------------------------------------------

看看下面一个很小的程序例子,它唯一做的事情就是返回一个数值到操作系统中。
UNIX系统通常返回0和1,这里我们使用42作为返回值。

[alert7@redhat]# set -o noclobber && cat > tiny.c << EOF

  /* tiny.c */
  int main(void) { return 42; }
EOF

[alert7@redhat]# gcc -Wall tiny.c
[alert7@redhat]# ./a.out ;echo $?
42

再用gdb看看,这个程序实在很简单吧
[alert7@redhat]# gdb a.out -q
(gdb) disass main
Dump of assembler code for function main:
0x80483a0 <main>:       push   %ebp
0x80483a1 <main+1>:     mov    %esp,%ebp
0x80483a3 <main+3>:     mov    $0x2a,%eax
0x80483a8 <main+8>:     jmp    0x80483b0 <main+16>
0x80483aa <main+10>:    lea    0x0(%esi),%esi
0x80483b0 <main+16>:    leave
0x80483b1 <main+17>:    ret

看看有多大
[alert7@redhat]# wc -c a.out
  11648 a.out
  
在原作者的机子上3998,在我的rh 2.2.14-5.0上就变成11648,好大啊,我们需要
使它变的更小。

[alert7@redhat]# gcc -Wall -s tiny.c
[alert7@redhat]# ./a.out ;echo $?
42
[alert7@redhat]# wc -c a.out
   2960 a.out
现在变成2960,小多了.
gcc -Wall -s tiny.c实际上等价于
gcc -Wall tiny.c
strip a.out 抛弃所有的标号

[alert7@redhat]# wc -c a.out
  11648 a.out
[alert7@redhat]# strip  a.out
[alert7@redhat]# wc -c a.out
   2960 a.out


下一步,我们来进行优化。


[alert7@redhat]# gcc -Wall -s -O3 tiny.c
[alert7@redhat]# wc -c a.out
   2944 a.out

我们看到,只比上面的小16个字节,所以以优化指令来减小大小是比较困难的。

很不幸,C程序在编译的时候编译器会增加一些额外的代码,所以接下来我们使用汇编来写程序。

如上一个程序,我们需要返回代码为42,我们只需要把eax设置为42就可以了。程序的
返回状态就是存放在eax中的,从上面一段disass main出来的汇编代码我们也应该知道。

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
  ; tiny.asm
  BITS 32
  GLOBAL main
  SECTION .text
  main:
                mov     eax, 42
                ret
EOF

编译并测试
[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s tiny.o
[alert7@redhat]# ./a.out ; echo $?
42

现在看看汇编代码有什么不同,看看它的大小
[alert7@redhat]# wc -c a.out
   2892 a.out

这样又减小了(2944-2892)52个字节. 但是,只要我们使用main()接口,就还会有许多额外的代码。
linker还会为我们加一个到OS的接口。事实上就是调用main().所以我们如何来去掉我们不需要的
代码呢。

linker默认使用的实际入口是标号_start.gcc联接时,它会自动包括一个_start的例程,设置argc和argv,
....,最后调用main().

所以让我们来看看,是否可以跳过这个,自己定义_start例程。

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
  ; tiny.asm
  BITS 32
  GLOBAL _start
  SECTION .text
  _start:
                mov     eax, 42
                ret
EOF

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s tiny.o
tiny.o: In function `_start':
tiny.o(.text+0x0): multiple definition of `_start'
/usr/lib/crt1.o(.text+0x0): first defined here
/usr/lib/crt1.o: In function `_start':
/usr/lib/crt1.o(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status


如何做才可以编译过去呢?
GCC有一个编译选项--nostartfiles

-nostartfiles
当linking时,不使用标准的启动文件。但是通常是使用的。

我们要的就是这个,再来:

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s -nostartfiles tiny.o
[alert7@redhat]# ./a.out ; echo $?
Segmentation fault (core dumped)
139

gcc没有报错,但是程序core dump了,到底发生了什么?

错就错在我们把_start看成了一个C的函数,然后试着从它返回。事实上它根本不是一个函数。
它仅仅是一个标号,它是被linker使用的一个程序入口点。当程序运行,它也就直接被调用。
假如我们来看,将看到在堆栈顶部的变量值为1,它的确非常的不象一个地址。事实上,在
堆栈那位置是我们程序的argc变量,之后是argv数组,包含NULL元素,接下来是envp环境变量。
所以,那个根本就不是返回地址。

因此,_start要退出,就要调用exit()函数。

事实上,我们实际调用的_exit()函数,因为exit()函数所要做的额外事情太多了,因为我们跳过了
lib库的启动代码,所以我们也可以跳过LIB库的shutdown代码。

好了,再让我们试试。调用_exit()函数,它唯一的参数就是一个整形。所以我们需要push一个数到
堆栈里,然后调用_exit().
(应该这样定义:EXTERN _exit)

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
  ; tiny.asm
  BITS 32
  EXTERN _exit
  GLOBAL _start
  SECTION .text
  _start:
                push    dword 42
                call    _exit
EOF

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s -nostartfiles tiny.o
[alert7@redhat]# ./a.out ; echo $?
42

yeah~~,成功了,来看看多大

[alert7@redhat]# wc -c a.out
   1312 a.out

不错不错,又减少了将近一半,:),有没有其他所我们感兴趣的gcc选项呢?

在-nostartfiles就有一个很另人感兴趣的选项:

-nostdlib
在linking的时候,不使用标准的LIB和启动文件。那些东西都需要自己指定传给
linker.
这个值得研究一下:

[alert7@redhat]# gcc -Wall -s -nostdlib tiny.o
tiny.o: In function `_start':
tiny.o(.text+0x6): undefined reference to `_exit'
collect2: ld returned 1 exit status


_exit()是一个库函数,但是加了-nostdlib 就不能使用了,所以我们必须自己处理,
首先,必须知道在linux下如何制造一个系统调用。


--------------------------------------------------------------------------------

象其他操作系统一样,linux通过系统调用来向程序提供基本的服务。
这包括打开文件,读写文件句柄,等等......

LINUX系统调用接口只有一个指令:int 0x80.所有的系统调用都是通过该接口。
为了制造一个系统调用,eax应该包含一个数字(该数字表明了哪个系统调用),其他寄存器
保存着参数。
假如系统调用使用一个参数,那么参数在ebx中;
假如使用两个参数,那么在ebx,ecx中
假如使用三个,四个,五个参数,那么使用ebx,ecx,esi

从系统调用返回时, eax 将包含了一个返回值。
假如错误发生,eax将是一个负值,它的绝对值表示错误的类型。

在/usr/include/asm/unistd.h中列出了不同的系统调用。
快速看一下将看到exit的系统调用号为1。它只有一个参数,该值会返回给父进程,该值会
被放到ebx中。

好了,现在又可以开工了:)

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
  ; tiny.asm
  BITS 32
  GLOBAL _start
  SECTION .text
  _start:
                mov     eax, 1
                mov     ebx, 42  
                int     0x80
EOF

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s -nostdlib tiny.o
[alert7@redhat]# ./a.out ; echo $?
42

看看大小

[alert7@redhat]# wc -c a.out
    416 a.out


现在可真是tiny,呵呵,那么还能不能更小呢?
如何使用更短的指令呢?

看看下面两段汇编代码:

  00000000 B801000000        mov        eax, 1
  00000005 BB2A000000        mov        ebx, 42
  0000000A CD80              int        0x80


  00000000 31C0              xor        eax, eax
  00000002 40                inc        eax
  00000003 B32A              mov        bl, 42
  00000005 CD80              int        0x80

很明显从功能上讲是等价的,但是下面一个比上面一个节约了5个字节。


使用gcc大概已经不能减少大小了,下面我们就使用linker--ld

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
  ; tiny.asm
  BITS 32
  GLOBAL _start
  SECTION .text
  _start:
            xor     eax,eax
                inc     eax
                mov     bl,42
                int     0x80

EOF
[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# ld -s tiny.o
[alert7@redhat]# wc -c a.out
    412 a.out

小了4个字节,应该是5个字节的,但是另外的一个字节被用来考虑对齐去了。

是否到达了极限了呢,能否更小?

hm.我们的程序代码现在只有7个字节长。是否ELF文件还有405字节的额外的负载呢 ?他们都是
些什么?

使用objdump来看看文件的内容:

[alert7@redhat]# objdump -x a.out | less
a.out: no symbols

a.out:     file format elf32-i386
a.out
architecture: i386, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x08048080

Program Header:
    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
         filesz 0x00000087 memsz 0x00000087 flags r-x

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000007  08048080  08048080  00000080  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .bss          00000001  08049087  08049087  00000087  2**0
                  CONTENTS
  2 .comment      0000001c  00000000  00000000  00000088  2**0
                  CONTENTS, READONLY

[译者注:在我的机子上多了个.bss节,我想可能是跟ld版本有关。所以在我系统上
     演示的一直比原作者上面的大:(
         看来要想更小的话,还是可以考虑找个低版本的编译:)
]

如上,完整的.text节为7个字节大,刚好如我们刚才所说。

但是还有其他的节,例如".comment",谁安排它的呢?".comment"节大小为28个字节。
我们现在不知道.comment节到底是什么东西,但是可以大胆的说,它是不必须的。

.comment节在文件偏移量为00000087 (16进制)
我们来看看是什么东西

[alert7@redhat]# objdump -s a.out

a.out:     file format elf32-i386

Contents of section .text:
8048080 31c040b3 2acd80                      1.@.*..
Contents of section .bss:
8049087 00                                   .
Contents of section .comment:
0000 00546865 204e6574 77696465 20417373  .The Netwide Ass
0010 656d626c 65722030 2e393800           embler 0.98.

哦,是nasm自己的一段信息,或许我们应该使用gas.......

假如我们:

[alert7@redhat]# set -o noclobber && cat > tiny.s << EOF
  .globl _start
  .text
  _start:
                xorl    %eax, %eax
                incl    %eax
                movb    $42, %bl
                int     $0x80
EOF

[alert7@redhat]# gcc -s -nostdlib tiny.S
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
    368 a.out

[译者注:在作者机子上这里大小没有变化,但在我的系统上,这里变成了368
    (跟作者的机子上一样了),比前面的所以的都要小
]


再用一下objdump,会有些不同:

  Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000007  08048074  08048074  00000074  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000000  0804907c  0804907c  0000007c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0804907c  0804907c  0000007c  2**2
                  ALLOC

没有了commnet节,但是多了两个无用的节,用来存储不存在的数据。而且那些节居然还是0长度。
他们使文件大小变大。

所以它们都是没有用的,我们如何来去掉它们呢?

我们需要准备一些elf文件格式的知识。虽然我也已经翻译过《ELF文件格式》 ,
http://www.xfocus.org/上可以找到,但是翻译的很垃圾,早已招人唾骂过了,
所以还是推荐大家看英文原版文档,而且是强烈推荐。



--------------------------------------------------------------------------------
elf文件格式英文文档下载地址:
ftp://tsx.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.gz.
或者 http://www.muppetlabs.com/~breadbox/software/ELF.txt.

基本的,我们需要知道如下知识:

每一个elf文件都是以一个ELF header的结构开始的。该结构为52个字节长,并且包含了一个
信息部分,这些信息部分描述了文件的内容。例如,前16个字节包含了一个“标识符”,它
包含了ELF文件的魔术数,但字节的标记表明是32位的还是64位的,小端序还是大端序,等等。
在elf header包含的其他的信息还有,例如:目标体系;ELF文件是否是可执行的还是OBJECT
文件还是一个共享的库;程序的开始地址;program header table和section header table
在文件的偏移量。


两个表可以出先在文件的任何地方, 但是以前经常是直接跟在ELF HEADER后面,后来出现在
文件的末尾或许是靠近末尾。两个表有相试的功能,都是为了甄别文件的组成。但是,
section header table更关注的是识别在程序中不同部分在什么地方,然而,program
header table描述的是哪里和如何把那些部分转载到内存中。
简单的说,section header table 是被编译器(compiler)和连接器(linker)使用,program
header table是被程序转载器(loader)使用。对object 文件,program header talbe是
可选的,实际上从来也没有出现过。同样的,对于可执行文件来说,section header table
也是可选的,但是它却总是存在于可执行文件中。

因此,对于我们的程序来说,seciton header table是完全没有用的,那些sections也不会
影响到程序内存的映象。

那么,到底如何去掉它们呢?

[1] [2]

责任编辑 webmaster

 
 
 
 
 
评论更多>>
 
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •