本文装载自:http://www.cnblogs.com/xuqiang/archive/2010/03/29/1953689.html
首先的感谢那些无私奉献的大牛们,深入Hello World下载地址在http://blog.linux.org.tw/~jserv/archives/001844.html。在上面
还有源码的下载地址链接,同时还要感谢那些网上的勤勤恳恳写blog的bloger们。
Hello World是学习程序设计语言的第一个程序浅出 Hello World。我们试图分析自linux上的Hello World运行的整个过程,主要
包括下面的几个过程:
1.hello程序的编译链接过程和hello上可执行文件格式
2.hello可执行程序的加载及如何开始执行
3.hello在内存中镜像
4.寻址
5.调度程序
6.内存管理
7.系统调用
8.hello程序卸载
首先是hello可执行文件的连接过程,然后hello可执行文件如何被加载到内存中,然后从那里开始执行?在执行的过程中,可
能需要寻址,如何实现?可能的内核调度如何实现?内存管理如何实现?系统调用如何实现?hello程序执行完成之后,kernel
执行了那些清理的工作?
vim hello.c
#include <stdio.h>
int main (int atgc, char* argv[])
{
printf ("Hello World");
return 0;
}
gcc hello.c -o hello
首先在Text Editor中编辑hello的source code,gcc在编译source code时,先执行预处理,完成将source code中的宏定义展开de
等功能,这个过程是由cpp完成,最终生成hello.i文件,然后由complier编译成hello.s,这个过程是由ccl完成,然后使用as
将上面的hello.s编译成hello.o,最后使用ld将上面生成的hello.o连接成可执行文件hello。
我们可以使用下面的命令来观察生成的hello程序:
file hello
hello: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux
2.6.15, not stripped
ldd hello
linux-gate.so.1 => (0xb7f01000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7d89000)
/lib/ld-linux.so.2 (0xb7f02000)
objdump -x hello
hello: file format elf32-i386
hello
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08048310
Program Header:
PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2
filesz 0x00000100 memsz 0x00000100 flags r-x
INTERP off 0x00000134 vaddr 0x08048134 paddr 0x08048134 align 2**0
filesz 0x00000013 memsz 0x00000013 flags r--
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x000004c0 memsz 0x000004c0 flags r-x
LOAD off 0x00000f0c vaddr 0x08049f0c paddr 0x08049f0c align 2**12
filesz 0x00000108 memsz 0x00000110 flags rw-
DYNAMIC off 0x00000f20 vaddr 0x08049f20 paddr 0x08049f20 align 2**2
filesz 0x000000d0 memsz 0x000000d0 flags rw-
NOTE off 0x00000148 vaddr 0x08048148 paddr 0x08048148 align 2**2
filesz 0x00000020 memsz 0x00000020 flags r--
STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
filesz 0x00000000 memsz 0x00000000 flags rw-
RELRO off 0x00000f0c vaddr 0x08049f0c paddr 0x08049f0c align 2**0
filesz 0x000000f4 memsz 0x000000f4 flags r--
反汇编:objdump -d hello
hello: file format elf32-i386
Disassembly of section .init:
08048294 <_init>:
8048294: 55 push %ebp
8048295: 89 e5 mov %esp,%ebp
8048297: 53 push %ebx
8048298: 83 ec 04 sub $0x4,%esp
804829b: e8 00 00 00 00 call 80482a0 <_init+0xc>
80482a0: 5b pop %ebx
80482a1: 81 c3 54 1d 00 00 add $0x1d54,%ebx
80482a7: 8b 93 fc ff ff ff mov -0x4(%ebx),%edx
80482ad: 85 d2 test %edx,%edx
80482af: 74 05 je 80482b6 <_init+0x22>
80482b1: e8 1e 00 00 00 call 80482d4 <__gmon_start__@plt>
80482b6: e8 e5 00 00 00 call 80483a0 <frame_dummy>
80482bb: e8 a0 01 00 00 call 8048460 <__do_global_ctors_aux>
80482c0: 58 pop %eax
80482c1: 5b pop %ebx
80482c2: c9 leave
80482c3: c3 ret
Disassembly of section .plt:
...
经过上面的观察,产生下面的疑问:ldd链接的文件的作用是什么?链接程序时发生了什么?可执行文件的格式是怎样的?为了解决上面的问题还
是得首先了解一下计算机程序的基础知识。
1.bss/data/code段,对于下面的程序:
int a;
int k = 3;
int foo (void)
{
return (k);
}
int b = 12;
int bar(void)
{
a = 0;
return (a + b);
}
在生成的汇编文件中,bss/data/text段如下:
------------------------
a = 0 bss
-----------------------
k = 3 b = 12 data
----------------------
ret text
----------------------
于是根据汇编文件生成的可执行文件镜像大致结构如下:
---------------------
12 data
3
---------------------
ret text
----------------------
header
---------------------
那么汇编文件中的bss段怎么没有了?在可执行文件的镜像中,在header中包含有该文件bss段的信息 。在kernel装载该hello可执行文件时,会产生
如下的内存镜像 :
----------------------- bss,kernel根据hello的文件头header来得到bss段的信息
0
-----------------------
12 3 data
----------------------
ret text
---------------------
上面的只是一个文件的情况,没有涉及到链接,那如果是两个.o文件,ld链接程序是如何链接程序的?首先需要说明的是每个object file都是具有相同的address space,在链接多个.o文件时,ld所作的工作就是分别将各个object file的bss text data段分别组成到新的bss text data段。最终在生成的elf
文件中:
-----------------------
elf header include magic number, file type, machine,...
-----------------------
program header table page size, virtual address memory segment, segment size
-----------------------
.text section code
-----------------------
.data section initialized data
-----------------------
.bss section bass data (0)
-----------------------
.symtab symbol table
------------------------
.rel.text relocation information for text section
-------------------------
.rel.data relocation information for .data section
-------------------------
.debug debug information
------------------------
section header table
------------------------
对于上面的elf文件elf header可以使用readelf -h filename来读取文件头。
readelf -h hello
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048310
Start of program headers: 52 (bytes into file)
Start of section headers: 5996 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 36
Section header string table index: 33
对于上面的输出需要注意的是Entry point address: 0x8048310,Entry point address定义了elf文件执行的开始地址。下面是详细的解释:
http://blog.sina.com.cn/s/blog_5b9ea9840100avgg.html###
When executed, program will start running from virtual address 0x80482c0 (see entry point address). The "0x" prefix here means it is a hexadecimal number. This address doesn't point to our main() procedure, but to a procedure named _start. Never felt you had created such thing? Of course you don't. _start procedure is created by the linker whose purpose is to initialize your program.
同时可以使用gdb反汇编:
gdb hello
(gdb) disassemble 0x8048310
Dump of assembler code for function _start:
0x08048310 <_start+0>: xor %ebp,%ebp
0x08048312 <_start+2>: pop %esi
0x08048313 <_start+3>: mov %esp,%ecx
0x08048315 <_start+5>: and $0xfffffff0,%esp
0x08048318 <_start+8>: push %eax
0x08048319 <_start+9>: push %esp
0x0804831a <_start+10>: push %edx
0x0804831b <_start+11>: push $0x80483f0
0x08048320 <_start+16>: push $0x8048400
0x08048325 <_start+21>: push %ecx
0x08048326 <_start+22>: push %esi
0x08048327 <_start+23>: push $0x80483c4
0x0804832c <_start+28>: call 0x80482e4 <__libc_start_main@plt>
0x08048331 <_start+33>: hlt
可见程序是在地址0x8048310开始执行(注意的是此时还是虚地址),然后通过设置调用printf函数前的相关工作,然后call 0x80482e4 <__libc_start_main@plt>调用printf函数。
综上,hello.c源程序,通过编译生成.o文件,然后在通过ld程序链接成elf可执行文件。ld在链接时,默认使用的ld script可以使用命令查看:
ld --verbose。关于链接脚本的解释参见:http://fxl.blogbus.com/logs/12451338.在ld script中指定elf文件的加载地址和虚地址,上面的两种地址
在一般的情况下是相同的,但是在一些嵌入式的程序中加载地址和执行地址是不同的。
既然在elf文件中定义了程序在加载的虚地址,那么程序是如何被加载的?加载的内存镜像又是什么样子的?如何查看实际的物理地址?呵呵,有时间
接着开始...