在linux平台上创建超小的ELF可执行文件创建时间:2001-09-04文章属性:整理文章来源:文章提交: (sztcww_at_sina.com)在linux平台上创建超小的ELF可执行文件作者:breadbox 原文时间:2001-9-4--------------------------------------------------------------------------------前言:    有些时候,文件的大小是很重要的,从这片文章中,也探讨了ELF文件格式内部的工作情况与LINUX的操作系统。该片文章向我们展示了如何构造一个超小的ELF可执行文件。文章中给出的这些example都是运行在intel 386体系的LINUX上。其他系统体系上或许也有同样的效果,但我不感肯定。我们的汇编代码使用的是Nasm写的,它的风格类似于X86汇编风格。NASM软件是免费的,可以从下面得到 在linux平台上创建超小的ELF可执行文件创建时间:2001-09-04文章属性:整理文章来源:文章提交: (sztcww_at_sina.com)在linux平台上创建超小的ELF可执行文件作者:breadbox 原文时间:2001-9-4--------------------------------------------------------------------------------前言:    有些时候,文件的大小是很重要的,从这片文章中,也探讨了ELF文件格式内部的工作情况与LINUX的操作系统。该片文章向我们展示了如何构造一个超小的ELF可执行文件。文章中给出的这些example都是运行在intel 386体系的LINUX上。其他系统体系上或许也有同样的效果,但我不感肯定。我们的汇编代码使用的是Nasm写的,它的风格类似于X86汇编风格。NASM软件是免费的,可以从下面得到 --------------------------------------------------------------------------------看看下面一个很小的程序例子,它唯一做的事情就是返回一个数值到操作系统中。UNIX系统通常返回0和1,这里我们使用42作为返回值。[alert7@redhat]# set -o noclobber && cat > tiny.c   /* 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 mainDump of assembler code for function main:0x80483a0 :       push   %ebp0x80483a1 :     mov    %esp,%ebp0x80483a3 :     mov    $0x2a,%eax0x80483a8 :     jmp    0x80483b0 0x80483aa :    lea    0x0(%esi),%esi0x80483b0 :    leave0x80483b1 :    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.cstrip 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   ; tiny.asm  BITS 32  GLOBAL main  SECTION .text  main:                mov     eax, 42                retEOF编译并测试[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   ; tiny.asm  BITS 32  GLOBAL _start  SECTION .text  _start:                mov     eax, 42                retEOF[alert7@redhat]# nasm -f elf tiny.asm[alert7@redhat]# gcc -Wall -s tiny.otiny.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)139gcc没有报错,但是程序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   ; tiny.asm  BITS 32  EXTERN _exit  GLOBAL _start  SECTION .text  _start:                push    dword 42                call    _exitEOF[alert7@redhat]# nasm -f elf tiny.asm[alert7@redhat]# gcc -Wall -s -nostartfiles tiny.o[alert7@redhat]# ./a.out ; echo $?42yeah~~,成功了,来看看多大[alert7@redhat]# wc -c a.out   1312 a.out不错不错,又减少了将近一半,:),有没有其他所我们感兴趣的gcc选项呢?在-nostartfiles就有一个很另人感兴趣的选项:-nostdlib 在linking的时候,不使用标准的LIB和启动文件。那些东西都需要自己指定传给linker.这个值得研究一下: [alert7@redhat]# gcc -Wall -s -nostdlib tiny.otiny.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   ; tiny.asm  BITS 32  GLOBAL _start  SECTION .text  _start:                mov     eax, 1                mov     ebx, 42                  int     0x80EOF[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   ; tiny.asm  BITS 32  GLOBAL _start  SECTION .text  _start:            xor     eax,eax                inc     eax                mov     bl,42                int     0x80EOF[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 | lessa.out: no symbolsa.out:     file format elf32-i386a.outarchitecture: i386, flags 0x00000102:EXEC_P, D_PAGEDstart address 0x08048080Program Header:    LOAD off    0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12         filesz 0x00000087 memsz 0x00000087 flags r-xSections: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.outa.out:     file format elf32-i386Contents of section .text:8048080 31c040b3 2acd80                      1.@.*..Contents of section .bss:8049087 00                                   .Contents of section .comment:0000 00546865 204e6574 77696465 20417373  .The Netwide Ass0010 656d626c 65722030 2e393800           embler 0.98.哦,是nasm自己的一段信息,或许我们应该使用gas.......假如我们:[alert7@redhat]# set -o noclobber && cat > tiny.s   .globl _start  .text  _start:                xorl    %eax, %eax                incl    %eax                movb    $42, %bl                int     $0x80EOF[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文件格式》 ,在上可以找到,但是翻译的很垃圾,早已招人唾骂过了,所以还是推荐大家看英文原版文档,而且是强烈推荐。--------------------------------------------------------------------------------elf文件格式英文文档下载地址:. 或者 .基本的,我们需要知道如下知识:每一个elf文件都是以一个ELF header的结构开始的。该结构为52个字节长,并且包含了一个信息部分,这些信息部分描述了文件的内容。例如,前16个字节包含了一个“标识符”,它包含了ELF文件的魔术数,但字节的标记表明是32位的还是64位的,小端序还是大端序,等等。在elf header包含的其他的信息还有,例如:目标体系;ELF文件是否是可执行的还是OBJECT文件还是一个共享的库;程序的开始地址;program header table和section header table在文件的偏移量。两个表可以出先在文件的任何地方, 但是以前经常是直接跟在ELF HEADER后面,后来出现在文件的末尾或许是靠近末尾。两个表有相试的功能,都是为了甄别文件的组成。但是,section header table更关注的是识别在程序中不同部分在什么地方,然而,programheader table描述的是哪里和如何把那些部分转载到内存中。简单的说,section header table 是被编译器(compiler)和连接器(linker)使用,program header table是被程序转载器(loader)使用。对object 文件,program header talbe是可选的,实际上从来也没有出现过。同样的,对于可执行文件来说,section header table也是可选的,但是它却总是存在于可执行文件中。因此,对于我们的程序来说,seciton header table是完全没有用的,那些sections也不会影响到程序内存的映象。那么,到底如何去掉它们呢?我们必须自己来构造程序的ELF HEADER.你也可以查看ELF文档和/usr/include/linux/elf.h得到相关信息,一个空的ELF可执行文件应该象如下:  BITS 32                  org     0x08048000    ehdr:                                                 ; Elf32_Ehdr                db      0x7F, "ELF", 1, 1, 1            ;   e_ident        times 9 db      0                dw      2                               ;   e_type                dw      3                               ;   e_machine                dd      1                               ;   e_version                dd      _start                          ;   e_entry                dd      phdr - $$                       ;   e_phoff                dd      0                               ;   e_shoff                dd      0                               ;   e_flags                dw      ehdrsize                        ;   e_ehsize                dw      phdrsize                        ;   e_phentsize                dw      1                               ;   e_phnum                dw      0                               ;   e_shentsize                dw      0                               ;   e_shnum                dw      0                               ;   e_shstrndx    ehdrsize      equ     $ - ehdr    phdr:                                                 ; Elf32_Phdr                dd      1                               ;   p_type                dd      0                               ;   p_offset                dd      $$                              ;   p_vaddr                dd      $$                              ;   p_paddr                dd      filesize                        ;   p_filesz                dd      filesize                        ;   p_memsz                dd      5                               ;   p_flags                dd      0x1000                          ;   p_align    phdrsize      equ     $ - phdr    _start:    ; your program here    filesize      equ     $ - $$该映象包含了一个ELF header ,没有section header table ,一个program header table 包含了一个入口。该入口指示程序转载器把完整的文件装载到内存(一般的是包含自己的ELF header 和program header table)开始地址为0x08048000(这是可执行文件装载的默认地址)的地方,并且开始执行_start处代码,_start紧跟着program header table.没有.data段,没有.bss段没有.comment段。好了,现在我们的程序就变成这样了:[alert7@redhat]# cat  tiny.asm ; tiny.asm                org     0x08048000ehdr:                                                 ; Elf32_Ehdr                db      0x7F, "ELF", 1, 1, 1            ;   e_ident        times 9 db      0                dw      2                               ;   e_type                dw      3                               ;   e_machine                dd      1                               ;   e_version                dd      _start                          ;   e_entry                dd      phdr - $$                       ;   e_phoff                dd      0                               ;   e_shoff                dd      0                               ;   e_flags                dw      ehdrsize                        ;   e_ehsize                dw      phdrsize                        ;   e_phentsize                dw      1                               ;   e_phnum                dw      0                               ;   e_shentsize                dw      0                               ;   e_shnum                dw      0                               ;   e_shstrndx  ehdrsize      equ     $ - ehdr  phdr:                                                 ; Elf32_Phdr                dd      1                               ;   p_type                dd      0                               ;   p_offset                dd      $$                              ;   p_vaddr                dd      $$                              ;   p_paddr                dd      filesize                        ;   p_filesz                dd      filesize                        ;   p_memsz                dd      5                               ;   p_flags                dd      0x1000                          ;   p_align  phdrsize      equ     $ - phdr_start:                mov     bl, 42                xor     eax, eax                inc     eax                int     0x80  filesize      equ     $ - $$[alert7@redhat]# nasm -f bin -o a.out tiny.asm[alert7@redhat]# chmod +x a.out[alert7@redhat]# ./a.out ; echo $?42再看看大小:[alert7@redhat]# wc -c a.out     93 a.out真是奇迹,才93个字节大小了。假如我们明白在可执行文件中的每个字节,我们或许还可以更小,也许很是极限了哦:)--------------------------------------------------------------------------------你可能已经注意到了:1)ELF文件的不同部分允许被定位在任何地方(除了ELF header,它必须放在文件的开始),并且它们可以交叠。2)事实上一些字段到目前还没有被用到。在鉴别文件字段最后有9个字节为0,我们的代码只有7个字节长,所以我们试图把代码放入鉴别文件字段最后9个字节中,还有2个剩余。.... [alert7@redhat]# cat  tiny.asm   ; tiny.asm    BITS 32                  org     0x08048000    ehdr:                                                 ; Elf32_Ehdr                db      0x7F, "ELF"                     ;   e_ident                db      1, 1, 1, 0  _start:       mov     bl, 42                xor     eax, eax                inc     eax                int     0x80        db    0                dw      2                               ;   e_type                dw      3                               ;   e_machine                dd      1                               ;   e_version                dd      _start                          ;   e_entry                dd      phdr - $$                       ;   e_phoff                dd      0                               ;   e_shoff                dd      0                               ;   e_flags                dw      ehdrsize                        ;   e_ehsize                dw      phdrsize                        ;   e_phentsize                dw      1                               ;   e_phnum                dw      0                               ;   e_shentsize                dw      0                               ;   e_shnum                dw      0                               ;   e_shstrndx    ehdrsize      equ     $ - ehdr    phdr:                                                 ; Elf32_Phdr                dd      1                               ;   p_type                dd      0                               ;   p_offset                dd      $$                              ;   p_vaddr                dd      $$                              ;   p_paddr                dd      filesize                        ;   p_filesz                dd      filesize                        ;   p_memsz                dd      5                               ;   p_flags                dd      0x1000                          ;   p_align    phdrsize      equ     $ - phdr    filesize      equ     $ - $$[alert7@redhat]# nasm -f bin -o a.out tiny.asm[alert7@redhat]# chmod +x a.out[alert7@redhat]# ./a.out ; echo $?42[alert7@redhat]# wc -c a.out     84 a.out现在我们的程序只有一个elf header和一个program header table入口,为了装载和运行程序,这些是我们必要的。所以现在我们不能减少了!除非....我们使elf header和program header table一部分重合或者说是交叠,有没有可能呢?答案当然是有的,注意我们的程序,就会注意到在elf header最后8个字节和program header table前8个字节是一样的,所以...[alert7@redhat]# cat tiny.asm   ; tiny.asm    BITS 32                  org     0x08048000    ehdr:                db      0x7F, "ELF"             ; e_ident                db      1, 1, 1, 0  _start:       mov     bl, 42                xor     eax, eax                inc     eax                int     0x80                db      0                dw      2                       ; e_type                dw      3                       ; e_machine                dd      1                       ; e_version                dd      _start                  ; e_entry                dd      phdr - $$               ; e_phoff                dd      0                       ; e_shoff                dd      0                       ; e_flags                dw      ehdrsize                ; e_ehsize                dw      phdrsize                ; e_phentsize  phdr:         dd      1                       ; e_phnum       ; p_type                                                ; e_shentsize                dd      0                       ; e_shnum       ; p_offset                                                ; e_shstrndx  ehdrsize      equ     $ - ehdr                dd      $$                                      ; p_vaddr                dd      $$                                      ; p_paddr                dd      filesize                                ; p_filesz                dd      filesize                                ; p_memsz                dd      5                                       ; p_flags                dd      0x1000                                  ; p_align  phdrsize      equ     $ - phdr    filesize      equ     $ - $$[alert7@redhat]# nasm -f bin -o a.out tiny.asm[alert7@redhat]# chmod +x a.out[alert7@redhat]# ./a.out ; echo $?42[alert7@redhat]# wc -c a.out     76 a.out现在已经不能够再更多的重叠那两个结构了,因为两个结构的字节没有再相同的了。但是,我们可以再构造这两个结构,使它们有更多的相同部分。到底linux会检查多少字段呢?例如,它会检查e_machine字段吗?事实上很另人惊讶,一些字段居然被默默的忽略了。因此:哪些东西才是ELF header中最重要的呢?最前的四个字节当然是的,它包含了一个魔术数,否则linux不会继续处理它。在e_ident字段的其他3个字节不被检查,那就意味着我们有不少于12个连续的字节我们可以设置为任意的值。e_type必须被设置为2(用来表明是个可执行文件),e_machine必须为3。就象e_ident中的版本号一样,e_version被完全的忽略。(这样做可以理解,因为现在只有一个版本的ELF标准)。e_entry当然要设置为正确的值,因为它指向程序的开始。毫无疑问,e_phoff应该是program header table在文件中的正确偏移量,e_phnum是program header table中所包含的正确的入口数。然而,e_flags没有被当前的Intel体系使用,所以我们应该可以重新利用。e_ehsize用来校验elf header所期望的大小,但是LINUX忽略了它。e_phentsize同样的确认program header table入口的大小。但是只有在2.2.17以后的2.2系列内核中这个字段才是被检查的。早于2.2的和2.4.0的内核是忽略它的。program header table又是如何呢?p_type必须是1(即PT_LOAD),表明这是个可载入的段。p_offset是开始装载的文件偏移量。同样的,p_vaddr是正确的装载地址。注意:我们没有要求把它装载到0x08048000. 可用的地址为0-0x80000000,并且要页对齐。文档上说p_paddr被忽略,因此这个字段更是可用的。p_filesz 指示了从文件中装载到内存中有多少字节,p_memsz指示了需要多大的内存段。因此,他们的值应该是相关的。p_flags指示了给于内存段什么权限。可设置读,写,执行,其他位也可能被设置,但是我们只需要最小权限。最后,p_align给出了对齐需求。该字段主要使用在当重定位段包含了与位置无关的代码时,岂今为止,可执行文件将被LINUX忽略这个字段。根据分析,我们从中可以看出一些必要的字段,一些无用的字段,这样,我们就可以重叠更多的字数了。[alert7@redhat]# cat  tiny.asm   ; tiny.asm◆     BITS 32                  org     0x00200000                  db      0x7F, "ELF"             ; e_ident                db      1, 1, 1, 0  _start:                mov     bl, 42                xor     eax, eax                inc     eax                int     0x80                db      0                dw      2                       ; e_type                dw      3                       ; e_machine                dd      1                       ; e_version                dd      _start                  ; e_entry                dd      phdr - $$               ; e_phoff  phdr:         dd      1                       ; e_shoff       ; p_type                dd      0                       ; e_flags       ; p_offset                dd      $$                      ; e_ehsize      ; p_vaddr                                                ; e_phentsize                dw      1                       ; e_phnum       ; p_paddr                dw      0                       ; e_shentsize                dd      filesize                ; e_shnum       ; p_filesz              &nbs
11-04 12:49
查看更多