歡迎您光臨本站 註冊首頁

在linux平台上創建超小的ELF可執行文件

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  整理:alert7(alert7)
來源:http://www.xfocus.org

在linux平台上創建超小的ELF可執行文件

作者:breadbox
原文
整理翻譯:alert7
主頁: http://www.xfocus.org/
時間:2001-9-4

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


前言:
有些時候,文件的大小是很重要的,從這片文章中,也探討了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
: push %ebp
0x80483a1 : mov %esp,%ebp
0x80483a3 : mov $0x2a,%eax
0x80483a8 : jmp 0x80483b0
0x80483aa : lea 0x0(%esi),%esi
0x80483b0 : leave
0x80483b1 : 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也不會
影響到程序內存的映象。

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

我們必須自己來構造程序的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 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 $ - $$


[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
; 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.

[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
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體系上根本就不具有執行保護功能,就是這個原因,才使得有人有
必要設計了類似堆棧不可運行的內核補丁程序。
]


[alert7@redhat]# 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的數值。

[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
52 a.out

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

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

真的是這樣嗎 ?

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

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


[alert7@redhat]# 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 $ - $$

[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
45 a.out


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

--------------------------------------------------------------------------------
一個45位元組大小的文件比一個用標準工具創建的最小可執行文件的1/8還要小,比用純C代碼
創建的1/50還要小。

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

以上程序中打上◆ 的程序,會使readelf core dump
[alert7@redhat]# 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

[alert7@redhat]# ls -l /usr/bin/readelf
-rwxr-xr-x 1 root root 132368 Feb 5 2000 /usr/bin/readelf

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

創建的這種超小的elf文件的確比較畸形,連objdump都不能dump它們了。
[alert7@redhat]# objdump -a a.out
objdump: a.out: File format not recognized





[火星人 ] 在linux平台上創建超小的ELF可執行文件已經有465次圍觀

http://coctec.com/docs/program/show-post-72168.html