http://blog.chinaunix.net/u2/77616/showart_1151031.html
本文记录了如何在linux下写一个不依赖任何库的c程序,并让它能运行起来。
试验环境: os: ubuntu8.04-i386, cpu: amd64
1. 操作系统从那儿开始执行程序?
在很多c语言的书都有介绍c程序是从main函数开始执行的。操作系统执行程序也是从
main函数开始吗?不是的,我们可以反汇编一段c的"hello,
world!"程序,从反汇编代码中能看到在main的前面还有一些代码。这些代码一般是由c的库函数提供,并由编译器插入到main函数前面,做一些初
始化工作,有关这方面可参看附1。操作系统执行c程序的入口点是_start。在linux下,用AT&T汇编写过程序的人应该都很熟悉以下的代
码段:
.text
.global _start
_start:
... #code
ret
在这里,_start就是整个汇编程序的入口点,也是操作系统执行程序的入口点。
2. 怎么调用c的main函数?
c程序是从main开始执行程序,从前面描述知道操作系统是从_start开始执行程序的。那么我们就应该想到main最终是由_start执行的。我们
也可以从反汇编出来的"hello,
world!"看到其实是__libc_start_main去调用的main。那么_start怎么把参数传递给main函数的呢?在
understanding linux kernel, 3rd ed, section
20.1中讲到了操作系统在执行程序时把命令行参数和环境变量放到用户的栈中,栈的层次如下:
| NULL | PAGE_OFFSET
---------------- evn_end
| 环境变量 |
| ... | evn_start
|-> ---------------- arg_end
| | 命令行参数 |
| | ... | arg_start
---|-> ----------------
| | | 动态链接表 |
| | ----------------
| | | NULL | <--- 注意在evnp的最后是一个NULL指针,在书上没有。
| | ----------------
| | | envp[] |
| ------------------- &envp[0]
| | NULL | <--- 注意在argv的最后也有一个NULL指针,在书上没有。
| ----------------
| | argv[] |
----------------------- &argv[0]
| argc |
---------------- start_stack, stack top(esp)
此图与书上的不完全一样,有几点要说明:
1. 在envp和argv数组的最后是一个空指针,但书上没有表现出来。
2. 在操作系统调用_start时栈中是没有返回地址的(书上则有),程序必须自己调用exit来结束程序。在有C库时是由编译器为我们在程序后面加入了exit系统调用。
所以我们在调用main之前必须把这个栈层次转换为main的参数形式。这个功能在boot.s中完成,代码如下:
.global _main
_main: lea 4(%esp), %eax # &argv
movl (%esp), %ebx # argc
movl %ebx, %ecx # calculate &envp = &argv + argc * 4 + 4(bytes)
shl $0x2, %ecx
addl %eax, %ecx
addl $4, %ecx
pushl %ecx # &envp, push the parameter of main into stack
pushl %eax
pushl %ebx
call main # call main
pushl %eax # main's return code
call _exit
ret
.global _start
_start:
jmp _main
ret
在_start中,我们直接跳到_main,然后从栈中取出操作系统放入的命令行参数和环境变量的地址,然后把它传递给main函数。在boot.s中的
所有ret都不会被执行到。要看懂这段代码,你应该知道c语言是如何处理参数传递的,linux在用户空间是如何执行系统调用的。这些知识在网上有很多介
绍,我们的重点是如何让一个没有库帮助的c程序跑起来,所以不对它们进行介绍。
3. main结束后我们还要做些什么?
在前面小节中我们可能已经注意到,当调用main后还有两行代码pushl %eax call _exit。它的作用就是用来结束程序。当程序有库帮助时,是由编译器把结束代码放到main后面的。以下是boot.s中的_exit:
.global _exit
_exit:
movl 4(%esp), %ebx # parameter
movl $1, %eax # exit's syscall no
int $0x80
ret
_exit的功能就是直接调用系统调用exit来结束整个程序,如果我们不这样做会怎么样?在2节中我们知道操作系统调用_start时是没有返回地址
的,如果没有_exit那么程序执行完main后就不知道跑那儿去了,结果就是程序崩溃。如果你在阅读1节时反汇编过"hello,world!"程序,
你可能还注意到在一个函数中如果调用了exit,编译器在这个函数的最后是不会插入ret返回函数的调用者的,因为一旦调用了exit,就意味着整个程序
的结束。
4. 示例程序。
最后我写了一个示例程序。程序由boot.s, syscall.c, syscall.h, main.c构成,boot.s的主要功能前面介绍过了,syscall.c主要实现write系统调用,main.c把命令行与环境变量输出到终端。
|
文件: |
nolibc.zip |
大小: |
1KB |
下载: |
下载 | |
附录:
1. Linux starand base, LSB-Core-generic.pdf and LSB-Core-IA32.pdf.
2. Understanding Linux Kernel, 3rd Ed.
3. 网络.
阅读(1062) | 评论(0) | 转发(0) |