在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 : jmp0x80483b0 0x80483aa :lea0x0(%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]# stripa.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: pushdword 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,文章整理:DIY部落 http://www.diybl.com (本站) 【点击打包该文章】 [1] [2] [3] [4] [5]