刘文学 + 原创作品转载请注明出处 http://blog.csdn.net/wdxz6547/article/details/51112486 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
本文我们想解决的问题:
核心问题
- 一个程序文件(.c, .cpp, .java .go) 文件是怎样变成二进制文件的.
- 二进制文件是怎样被载入并运行的.
辅助问题
- 一个二进制文件的格式是怎么样的? 不同的语言的二进制文件格式会不同么? 主要探讨 ELF 格式文件
- 静态链接和动态链接的差别
- 可运行文件与进程的地址空间的映射关系
一个程序文件(.c, .cpp, .java .go) 文件是怎样变成二进制文件的
C 文件 –> 预处理 –> 汇编成汇编代码(.asm) –> 汇编成目标码(.o) –> 链接成可运行文件
- 预处理: 把 include 的文件包括进来及宏定义替换
gcc -E -o hello.cpp hello.c
- 编译
gcc -x cpp-output -S -o hello.s hello.cpp
- 汇编: 生成二进制文件(之前都是可读的文本文件, 此步骤生成二进制文件,
包括一些机器指令, 但不是可运行文件)
gcc -x assembler -c hello.s -o hello.o
- 链接(ELF 格式文件)
gcc -o hello hello.o //默认动态
gcc -o hello.static hello.o -static //静态
$ readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 320 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 10
$ readelf -h hello
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400440
Start of program headers: 64 (bytes into file)
Start of section headers: 4504 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 27
$ readelf -h hello.static
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400f4e
Start of program headers: 64 (bytes into file)
Start of section headers: 789968 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 6
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28
$ ldd hello
linux-vdso.so.1 => (0x00007fff06ffe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc6c2d40000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc6c3125000)
可运行文件格式
具体參考这里
A.out --> COFF --> PE (Windows)
--> ELF (Linux)
ABI 与目标文件格式关系: 目标文件一般也叫ABI 文件, 实际目标文件已经是二进制兼容的格式(即该二进制文件已经适应到某一种 CPU 体系结构的二进制指令).
ELF
Object 參与程序的链接(创建一个程序)和运行(运行一个程序)
Linking View Execution View
============ ==============
ELF header ELF header
Program header table (optional) Program header table
Section 1 Segment 1
… Segment 2
Section n …
Section header table Section header table (optional)
ELF 头在文件的开头, 保存了线路图(road map), 描写叙述了文件的组织情况
程序头表告诉系统怎样创建一个进程的内存映像,
section 头表: 包括描写叙述文件 sections 部分, 每一个 section 在这个表中都有一个入口;
每一个入口给出了该 section 的名字, 大小等信息
可运行文件与进程地址空间的映射关系
当创建或添加一个进程映像的时候, 系统理论上将拷贝一个文件的段到一个虚拟的内存段
File Offset File Virtual Address
=========== ==== ===============
0 ELF header
Program header table
Other information
0x100 Text segment 0x8048100
...
0x2be00 bytes 0x8073eff //8048100 + 2be00
0x2bf00 Data segment 0x8074f00
...
0x4e00 bytes 0x8079cff
0x30d00 Other information
...
静态链接的 ELF 可运行文件与进程的地址空间的关系
一般静态链接会将全部的代码放在一个代码段
动态链接的进程会有多个代码段
二进制文件是怎样被载入并运行的
由前面章节的知识推測, 运行一个二进制文件的基本思路:
开启一个新的进程, 该进程主要工作就是载入并运行可运行文件, 主要包括载入与运行两部分; 当代码运行到载入可运行文件的时候, 调用 execve 系统调用. 该调用应该将可运行文件的内容载入到内存而且重置堆栈, sp, ip, 等关键寄存器, 之后运行可运行文件里指定的代码,这里必定涉及到寄存器相关的操作.
这里将以 bash 为例解释一个程序的运行的过程(其它相似).
- Shell 将命令行參数和环境參数传递给Bash 的 main 函数, main 函数将命令行解析后传递给系统调用 execve
首先, 我们在 bash 中输入一个命令
$./hello
因为 bash 也是 C 程序, 因此它也一定有 main 函数. 关于 shell 怎样到达 execve 的过程略.
假设你想看你运行的程序在 execve 是怎么运行的,
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
$ strace ./hello
execve("./hello", ["./hello"], [/* 78 vars */]) = 0
brk(0) = 0xacd000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f08182cc000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=122541, ...}) = 0
mmap(NULL, 122541, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f08182ae000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF211 3 >