手机站
网通分站
电信主站
密 码:
用户名:
当前位置 : 主页>网络编程>其它>列表

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

来源:互联网 作者:西部数码 时间:2008-04-09
西部数码-全国虚拟主机10强!40余项虚拟主机管理功能,全国领先!双线多线虚拟主机南北访问畅通无阻!免费赠送企业邮局,.CN域名,自助建站480元起,免费试用7天,满意再付款! P4主机租用799元/月.月付免压金!
作者:breadbox <breadbox@muppetlabs.com>
原文<A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux >
整理翻译:alert7 <alert7@21cn.com>
来源: 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 <main>: push 雙
0x80483a1 <main 1>: mov %esp,雙
0x80483a3 <main 3>: mov $0x2a,陎
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环境变量。
所以,那个根本就不是返回地址。

文章整理:西部数码--专业提供域名注册虚拟主机服务
http://www.west263.com
以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢!