1. 程式人生 > >將linux檔案大小優化進行到底

將linux檔案大小優化進行到底

前言:

有些時候,檔案的大小是很重要的,從這片文章中,也探討了ELF檔案格式內部的工作
情況與LINUX的作業系統。該片文章向我們展示瞭如何構造一個超小的ELF可執行檔案。

文章中給出的這些example都是執行在intel 386體系的LINUX上。其他系統體系上或許也有同樣的
效果,但我不感肯定。

我們的彙編程式碼使用的是Nasm寫的,它的風格類似於X86彙編風格。
NASM軟體是免費的,可以從下面得到
http://www.web-sites.co.uk/nasm/--------------------------------------------------------------------------------

看看下面一個很小的程式例子,它唯一做的事情就是返回一個數值到作業系統中。
UNIX系統通常返回0和1,這裡我們使用42作為返回值。

[[email protected]]# set -o noclobber && cat > tiny.c << EOF
  /* tiny.c */
  int main(void) { return 42; }
EOF

[[email protected]]# gcc -Wall tiny.c
[[email protected]]# ./a.out ;echo $?
42

再用gdb看看,這個程式實在很簡單吧
[[email protected]
]# 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

看看有多大
[[email protected]]# wc -c a.out
  11648 a.out
 
在原作者的機子上3998,在我的rh 2.2.14-5.0上就變成11648,好大啊,我們需要
使它變的更小。

[[email protected]]# gcc -Wall -s tiny.c
[[email protected]]# ./a.out ;echo $?
42
[[email protected]]# wc -c a.out
   2960 a.out
現在變成2960,小多了.
gcc -Wall -s tiny.c實際上等價於
gcc -Wall tiny.c
strip a.out 拋棄所有的標號

[[email protected]]# wc -c a.out
  11648 a.out
[[email protected]]# strip  a.out
[[email protected]]# wc -c a.out
   2960 a.out


下一步,我們來進行優化。


[[email protected]]# gcc -Wall -s -O3 tiny.c
[[email protected]]# wc -c a.out
   2944 a.out

我們看到,只比上面的小16個位元組,所以以優化指令來減小大小是比較困難的。

很不幸,C程式在編譯的時候編譯器會增加一些額外的程式碼,所以接下來我們使用匯編來寫程式。

如上一個程式,我們需要返回程式碼為42,我們只需要把eax設定為42就可以了。程式的
返回狀態就是存放在eax中的,從上面一段disass main出來的彙編程式碼我們也應該知道。

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

編譯並測試
[[email protected]]# nasm -f elf tiny.asm
[[email protected]]# gcc -Wall -s tiny.o
[[email protected]]# ./a.out ; echo $?
42

現在看看彙編程式碼有什麼不同,看看它的大小
[[email protected]]# wc -c a.out
   2892 a.out

這樣又減小了(2944-2892)52個位元組. 但是,只要我們使用main()介面,就還會有許多額外的程式碼。
linker還會為我們加一個到OS的介面。事實上就是呼叫main().所以我們如何來去掉我們不需要的
程式碼呢。

linker預設使用的實際入口是標號_start.gcc聯接時,它會自動包括一個_start的例程,設定argc和argv,
....,最後呼叫main().

所以讓我們來看看,是否可以跳過這個,自己定義_start例程。

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

[[email protected]]# nasm -f elf tiny.asm
[[email protected]]# 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時,不使用標準的啟動檔案。但是通常是使用的。

我們要的就是這個,再來:

[[email protected]]# nasm -f elf tiny.asm
[[email protected]]# gcc -Wall -s -nostartfiles tiny.o
[[email protected]]# ./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)

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

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

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

[[email protected]]# wc -c a.out
   1312 a.out

不錯不錯,又減少了將近一半,:),有沒有其他所我們感興趣的gcc選項呢?

在-nostartfiles就有一個很另人感興趣的選項:

-nostdlib
在linking的時候,不使用標準的LIB和啟動檔案。那些東西都需要自己指定傳給
linker.
這個值得研究一下:

[[email protected]]# 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中。

好了,現在又可以開工了:)

[[email protected]]# 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

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

看看大小

[[email protected]]# 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

[[email protected]]# 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
[[email protected]]# nasm -f elf tiny.asm
[[email protected]]# ld -s tiny.o
[[email protected]]# wc -c a.out
    412 a.out

小了4個位元組,應該是5個位元組的,但是另外的一個位元組被用來考慮對齊去了。

是否到達了極限了呢,能否更小?

hm.我們的程式程式碼現在只有7個位元組長。是否ELF檔案還有405位元組的額外的負載呢 ?他們都是
些什麼?

使用objdump來看看檔案的內容:

[[email protected]]# 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進位制)
我們來看看是什麼東西

[[email protected]]# objdump -s a.out

a.out:     file format elf32-i386

Contents of section .text:
8048080 31c040b3 2acd80                      [email protected]*..
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.......

假如我們:

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

[[email protected]]# gcc -s -nostdlib tiny.S
[[email protected]]# ./a.out ; echo $?
42
[[email protected]]# 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也不會
影響到程式記憶體的映象。

那麼,到底如何去掉它們呢?

我們必須自己來構造程式的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段。

好了,現在我們的程式就變成這樣了:

[[email protected]]# cat  tiny.asm
; tiny.asm
                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:
                mov     bl, 42
                xor     eax, eax
                inc     eax
                int     0x80

  filesize      equ     $ - $$


[[email protected]]# nasm -f bin -o a.out tiny.asm
[[email protected]]# chmod +x a.out
[[email protected]]# ./a.out ; echo $?
42


再看看大小:

[[email protected]]# wc -c a.out
     93 a.out


真是奇蹟,才93個位元組大小了。

假如我們明白在可執行檔案中的每個位元組,我們或許還可以更小,也許很是極限了哦:)


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

你可能已經注意到了:
1)ELF檔案的不同部分允許被定位在任何地方(除了ELF header,它必須放在檔案的開始),
並且它們可以交疊。
2)事實上一些欄位到目前還沒有被用到。

在鑑別檔案欄位最後有9個位元組為0,我們的程式碼只有7個位元組長,所以我們試圖把程式碼放入
鑑別檔案欄位最後9個位元組中,還有2個剩餘。....


[[email protected]]# 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     $ - $$


[[email protected]]# nasm -f bin -o a.out tiny.asm
[[email protected]]# chmod +x a.out
[[email protected]]# ./a.out ; echo $?
42
[[email protected]]# wc -c a.out
     84 a.out


現在我們的程式只有一個elf header和一個program header table入口,為了裝載和執行程式,
這些是我們必要的。所以現在我們不能減少了!除非....


我們使elf header和program header table一部分重合或者說是交疊,有沒有可能呢?

答案當然是有的,注意我們的程式,就會注意到在elf header最後8個位元組和program header table
前8個位元組是一樣的,所以...

[[email protected]]# 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     $ - $$


[[email protected]]# nasm -f bin -o a.out tiny.asm
[[email protected]]# chmod +x a.out
[[email protected]]# ./a.out ; echo $?
42
[[email protected]]# 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忽略這個欄位。

根據分析,我們從中可以看出一些必要的欄位,一些無用的欄位,這樣,我們就可以重疊更多的
字數了。

[[email protected]]# 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
                                                ; e_shstrndx
                dd      filesize                                ; p_memsz
                dd      5                                       ; p_flags
                dd      0x1000                                  ; p_align
 
  filesize      equ     $ - $$


正如你看到的,program header table的前12個位元組重疊在ELF header的最後12個位元組裡。
相當的吻合。ELF header重複中只有兩部分會有麻煩。一是e_phnum欄位,相對應的是p_paddr
是會被忽略。第二個是e_phentsize欄位,它和p_vaddr前兩個位元組相一致,為了這個相一致,
使用了非標準的載入地址0x00200000,那麼前面的兩個位元組就是0x0020.

[[email protected]]# nasm -f bin -o a.out tiny.asm
[[email protected]]# chmod +x a.out
[[email protected]]# ./a.out ; echo $?
42
[[email protected]]# wc -c a.out
     64 a.out

well,現在大小為64位元組了

如果我們使 program header table完全放在ELF header中,那麼,呵呵,大小就可以更小了,
但是這樣做行嗎?

是的,是可能的。使program header table從第四個位元組就開始,精心構造可執行的ELF檔案。

我們注意到:
第一P_memsz指出了為記憶體段分配多少記憶體。明顯的,它必須至少跟P_filesz一樣大,
當然更大是沒有關係的。

第二, 可執行位可以從p_flags欄位中丟棄,linux會為我們設定它的。為什麼這樣會工作呢?
作者說不知道,又猜測了原因說是否因為入口指標指向了該段?

[★譯者注:
    但我知道,linux根本就沒有為我們設定p_flags欄位中的可執行位,可以工作,
    只是因為Intel體系上根本就不具有執行保護功能,就是這個原因,才使得有人有
    必要設計了類似堆疊不可執行的核心補丁程式。
]


[[email protected]]# cat  tiny.asm
  ; tiny.asm
 
  BITS 32
 
                org     0x00001000
 
                db      0x7F, "ELF"             ; e_ident
                dd      1                                       ; p_type
                dd      0                                       ; p_offset
                dd      $$                                      ; p_vaddr
                dw      2                       ; e_type        ; p_paddr
                dw      3                       ; e_machine
                dd      filesize                ; e_version     ; p_filesz
                dd      _start                  ; e_entry       ; p_memsz
                dd      4                       ; e_phoff       ; p_flags
  _start:
                mov     bl, 42                  ; e_shoff       ; p_align
                xor     eax, eax
                inc     eax                     ; e_flags
                int     0x80
                db      0
                dw      0x34                    ; e_ehsize
                dw      0x20                    ; e_phentsize
                dw      1                       ; e_phnum
                dw      0                       ; e_shentsize
                dw      0                       ; e_shnum
                dw      0                       ; e_shstrndx
 
  filesize      equ     $ - $$

p_flags欄位從5變為4,這個4也是e_phoff欄位的值,它給出了program header table在檔案中
的偏移量。程式碼被放在從e_shoff 開始到e_flags內部結束。

注意到:裝載地址被改變了更低了。只是為了保持e_entry的值到一個比較合適的小值,它剛好
也是P_mensz的數值。

[[email protected]]# nasm -f bin -o a.out tiny.asm
[[email protected]]# chmod +x a.out
[[email protected]]# ./a.out ; echo $?
42
[[email protected]]# wc -c a.out
     52 a.out

現在,程式程式碼本身和program header table完全嵌入了ELF header,我們的可執行檔案現在和
elf header一樣大。而且可以正常執行。

最後,我們不禁還要問,是否到達了最小的極限呢?畢竟,我們需要一個完整的ELF header,否則
linux不會給我們執行的機會。

真的是這樣嗎 ?

錯了,我們還可以運用最後一招卑鄙的哄騙技術了。

如果檔案大小還沒有整個ELF header大的話,linux還是會執行它的。並且把那些少的位元組填充為
0。我們在檔案的最後有不少於7個0,可以丟棄。


[[email protected]]# cat  tiny.asm
  ; tiny.asm
 
  BITS 32
 
                org     0x00001000
 
                db      0x7F, "ELF"             ; e_ident
                dd      1                                       ; p_type
                dd      0                                       ; p_offset
                dd      $$                                      ; p_vaddr
                dw      2                       ; e_type        ; p_paddr
                dw      3                       ; e_machine
                dd      filesize                ; e_version     ; p_filesz
                dd      _start                  ; e_entry       ; p_memsz
                dd      4                       ; e_phoff       ; p_flags
  _start:
                mov     bl, 42                  ; e_shoff       ; p_align
                xor     eax, eax
                inc     eax                     ; e_flags
                int     0x80
                db      0
                dw      0x34                    ; e_ehsize
                dw      0x20                    ; e_phentsize
                db      1                       ; e_phnum
                                                ; e_shentsize
                                                ; e_shnum
                                                ; e_shstrndx
 
  filesize      equ     $ - $$

[[email protected]]# nasm -f bin -o a.out tiny.asm
[[email protected]]# chmod +x a.out
[[email protected]]# ./a.out ; echo $?
42
[[email protected]]# wc -c a.out
     45 a.out


討論到此,一個elf可執行檔案最小大小為45 bytes,我們被迫終止我們的討論了。

--------------------------------------------------------------------------------
一個45位元組大小的檔案比一個用標準工具建立的最小可執行檔案的1/8還要小,比用純C程式碼
建立的1/50還要小。

這片文章中的一半ELF欄位變數違反了標準的ELF規範,

以上程式中打上◆ 的程式,會使readelf core dump
[[email protected]]# readelf -a a.out
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 b3 2a 31 c0 40 cd 80 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       179
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x200008
  Start of program headers:          32 (bytes into file)
  Start of section headers:          1 (bytes into file)
  Flags:                             0x0
  Size of this header:               0 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         1
  Size of section headers:           0 (bytes)
  Number of section headers:         64
  Section header string table index: 0
readelf: Error: Unable to read in 0 bytes of section headers

Program Header:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x00200000 0x00000001 0x00040 0x00040 R E 0x1000

There is no dynamic segment in this file.
Segmentation fault (core dumped)

呵呵,居然出現了可愛的core dumped

[[email protected]]# ls -l /usr/bin/readelf
-rwxr-xr-x    1 root     root       132368 Feb  5  2000 /usr/bin/readelf

:(不是帶s位的,也就懶的去看它到底哪裡出問題了。

建立的這種超小的elf檔案的確比較畸形,連objdump都不能dump它們了。
[[email protected]]# objdump -a a.out
objdump: a.out: File format not recognized

http://www.xfocus.org
文章提交:[email protected]
在linux平臺上建立超小的ELF可執行檔案

作者:breadbox <[email protected]>
原文<A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux >
整理翻譯:alert7    <
[email protected]>
主頁:   
http://www.xfocus.org/

相關推薦

linux檔案大小優化進行到底

前言: 有些時候,檔案的大小是很重要的,從這片文章中,也探討了ELF檔案格式內部的工作情況與LINUX的作業系統。該片文章向我們展示瞭如何構造一個超小的ELF可執行檔案。文章中給出的這些example都是執行在intel 386體系的LINUX上。其他系統體系上或許也有同樣的

Linux 檔案大小查詢排序

du -sh 檔案大小查詢; 1、當前目錄的大小; du -sh | sort 2、當前 目錄下的檔案大小; ls -lsh 3、當前目錄 下的檔案大小排序; du -sh * |sort -n 4、查詢當前的最大檔案; du -sh |sort -nr |head 5、同一目錄下檔案 最

lucene索引檔案大小優化小結

      隨著業務快速發展,基於lucene的索引檔案zip壓縮後也接近了GB量級,而保持索引檔案大小為一個可以接受的範圍非常有必要,不僅可以提高索引傳輸、讀取速度,還能提高索引cache效率(lucene開啟索引檔案的時候往往會進行快取,比如MMapDirectory通過記憶體對映方式進行快取)。   

linux檔案打包到windows

很多時候我們需要將windows下好的壓縮包或檔案上傳到linux伺服器,我們可以通過xshell等終端先連線上伺服器,再用rz命令來上傳(PS:大檔案用rz -be或rz -bye,保證網速良好,親測可用~),但有些時候我們也會碰到把linux下的檔案打包到windows

檔案處理 txt檔案當中資料取出進行以郵箱分類處理 郵箱拿出來寫入對應的資料夾 分資料夾進行儲存 新手求吐槽優化

import os pathemail = r'E:\python\7.11\郵箱.txt' with open(pathemail,'r',encoding='utf-8') as f: flist = f.readlines() for i in range(len(fli

Linux - swap 大小改變及優化

linu ech usr pin 文件大小 conf 當前 表示 bin swap 大小改變 假設當前系統swap大小為4GB,想增加swap大小為5GB。 步驟如下: 查看當前swap大小 free -h 增加1GBswap大小,假設文件路徑及名稱:/snow/s

Linux創造固定的檔案大小-預分配磁碟空間

一、課前預習 寫本篇文章的目的很簡單,防止採坑、防止採坑、防止採坑我們在開發程式的過程中,往往需要預分配磁碟空間,防止因磁碟空間不夠而引發程式異常問題(已踩過坑), 現網查閱資料,有些預分配磁碟空間的方法不正確,在這裡特別記錄一下, 除此之外,把正確的預分配的方法和大家分享一下,如果其他人有建議,歡迎拍磚狠

嵌入式Linux標準IO,獲取檔案大小fgetc(),定位流獲取檔案大小fteel()、rewind()/fseek(),處理錯誤資訊perror()/strerror()

#include <stdio.h> #include <errno.h> #include <string.h> int get_file_size(const char *file); int main(int argc, const char *a

linux檢視硬碟空間 檔案大小

    du,disk usage,是通過搜尋檔案來計算每個檔案的大小然後累加,du能看到的檔案只是一些當前存在的,沒有被刪除的。他計算的大小就是當前他認為存在的所有檔案大小的累加和   df,disk free,通過檔案系統來快速獲取空間大小的資訊,當我們刪除一個檔案的

如何HDFS檔案系統掛載到Linux本地檔案系統

本文轉自https://cloud.tencent.com/developer/article/1078538,如果侵權請聯絡我刪除。 1.文件編寫目的 Hadoop支援通過NFSv3掛載HDFS檔案系統到本地目錄,允許使用者像訪問本地檔案系統一樣訪問HDFS,對於普通使用者來說大大的簡

CAD檔案轉換出的圖片大小如何進行改變?

CAD檔案轉換出的圖片大小如何進行改變?我們在將一張CAD圖紙檔案轉換成PDF格式的時候,如果需要將這張圖片設定成特定的格式大小應該如何進行此項操作呢,今天小編就要來教教大家如何改變一張轉換好的CAD圖紙大小的全部操作步驟,希望大家能夠進行採納! 第一步:需要進行圖紙格式轉換的,必須要有一款好用的轉

linuxpdf檔案轉換成swf檔案

用PHP用到了線上瀏覽pdf檔案的功能,js播放外掛需要同時將pdf檔案轉換成swf檔案兩者兼用才能達到效果。 安裝swftools 中文支援安裝: mkdir –p /usr/share/xpdf cd /usr/share/xpdf/ 下載中文支援及字型庫 wget ftp:/

資料庫查詢資料封裝到XML檔案中,進行格式化處理,並進行加密操作,和解密操作

1.pom檔案 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7&l

如何本地檔案通過終端上傳到linux伺服器 /伺服器/阿里雲

scp -P 埠 c://xxxx.txt [email protected]:/home/root 注意: -P 大寫 -i 公鑰 (我是將檔案上傳到阿里雲)   (1)在本地的終端下,而不是在伺服器上。在本地的終端上才能將本地的檔案拷入伺服器。 (2)

linux】---sftp協議windows檔案拷貝到linux

1.通過sftp協議,安全連線到遠端伺服器,建議使用xshell的連線, 也可以使用sftp  使用者名稱@主機名 實現linux機器與linux機器之間檔案的上傳操作 2.上傳檔案 可以上傳到指定目錄 sftp> cd /home/  sftp>

SecureCRT本地檔案上傳至centos7(linux)伺服器上

方法一: 上傳檔案只需在shell終端模擬器中輸入命令“rz”,即可從彈出的對話方塊中選擇本地磁碟上的檔案,利用Zmodem上傳到伺服器當前路徑下。 下載檔案只需在shell終端模擬器中輸入命令“sz 檔名”,即可利用Zmodem將檔案下載到本地某目錄下。 通過“File

Linux使用scp命令進行檔案遠端拷貝詳解

前言 scp是 secure copy的縮寫, scp是Linux系統下基於ssh登陸進行安全的遠端檔案拷貝命令。Linux的scp命令可以在Linux伺服器之間複製檔案和目錄。 使用語法: scp  [引數] [源路徑] @IP:/目標路徑 scp 引數如下: -1: 強制sc

linux查詢當前目錄下的所有檔案大小和清空檔案內容命令

  查詢當前目錄下的所有檔案大小:du -sh *  刪除檔案:rm - rf xxx.log 列印日誌:tail -f xxx.log 檢視程序ps -ef | grep java | grep -v grep 殺死客戶端kill -9 14569 - 啟動

EasyNVR在Linux系統下錄影檔案與EasyNVR執行分離

問題背景 在工控機上執行EasyNVR,WEB訪問出現裝置線上,但是視訊沒有快照和無法正常直播 問題原因分析 通過上工控機發現是由於磁碟空間被佔滿導致的軟體執行收到影響。 解決問題分析 由於錄影檔案沾滿磁碟導致的EasyNVR軟體執行收到影響,我們可以將軟體

linux 根據檔案大小查詢檔案

linux下的find命令用來查詢檔案,通過man find就知道它是無所不能的。所以按照檔案大小來查詢檔案就不在話下。從man find搜尋size,可以看到如下資訊: -size n[cwbkMG] File uses n units of space. The following suffix