TheRiver | blog

You have reached the world's edge, none but devils play past here

0%

操作系统-编译链接

参考

主要参考了:陈向群操作系统, 程序员的自我修养

Gcc 编译的背后

GCC编程四个过程:预处理-编译-汇编-链接

helloworld程序的运行过程

1
2
3
4
5
6
7
8

#include <stdio.h>
int main(int argc, char *argv[])
{
puts("hello world");
return 0;
}

  • 用户告诉操作系统执行helloworld程序
  • 操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址
  • 操作系统:创建一个新的进程,并将helloworld可执行文件映射到该进程结构,表示由该进程执行helloworld程序
  • 操作系统:为helloworld程序设置CPU上下文环境,并跳到程序开始处
  • 执行helloworld程序的第一条指令,发生缺页异常
  • 操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序
  • helloworld程序执行puts函数(系统调用),在显示器上写一字符串
  • 操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
  • 操作系统:控制设备的进程告诉设备的窗口系统它要显示字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
  • 视频硬件将像素转换成显示器可接收的一组控制/数据信号
  • 显示器解释信号,激发液晶屏
  • OK!!!我们在屏幕上看到了“hello world”

操作系统的5大功能(从资源管理的角度)

1 进程/线程管理(CPU管理)

进程线程状态、控制、同步互斥、通信、调度、……

2 存储管理

分配/回收、地址转换、存储保护、内存扩充、……

3 文件管理

文件目录、文件操作、磁盘空间、文件存取控制、……

4 设备管理

设备驱动、分配回收、缓冲技术、……

5 用户接口

系统命令、编程接口

操作系统的主要特征

1 并发

2 共享

3 虚拟

4 随机/异常

linux内核组件

linux_kernel.jpg


可执行文件的装载与进程

可执行文件:

[root@localhost bin]# file cgdb
cgdb: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs)
for GNU/Linux 2.6.32, BuildID[sha1]=0x7e94e0eb5975bd6a1eb4c7a8c0af6b77475f4362, not stripped

[root@localhost bin]# file ShadowsocksR-dotnet2.0.exe
ShadowsocksR-dotnet2.0.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly,for MS Windows

进程的建立:

  • 创建一个独立的虚拟地址空间(页目录) 虚拟空间和物理内存的映射关系
  • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系 虚拟空间和可执行文件的映射关系
  • 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行

可执行文件在装载时实际是被映射的虚拟空间,所以可执行文件很多时候又被叫做映像文件

gcc编译的四步

1
2
3
4
5
6
7
8
9
10

#include "stdio.h"

int main()
{
puts("hello, it,s me!");
return 0;
}


预处理阶段

头文件的包含,宏定义的扩展,条件编译的选择

gcc -E test.c -o test.i
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

# 1 "test.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<命令行>" 2
# 1 "test.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 375 "/usr/include/features.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 392 "/usr/include/sys/cdefs.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 393 "/usr/include/sys/cdefs.h" 2 3 4
# 376 "/usr/include/features.h" 2 3 4
# 399 "/usr/include/features.h" 3 4
# 1 "/usr/include/gnu/stubs.h" 1 3 4
# 10 "/usr/include/gnu/stubs.h" 3 4
# 1 "/usr/include/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/gnu/stubs.h" 2 3 4
# 400 "/usr/include/features.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4





# 1 "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h" 1 3 4
# 212 "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h" 3 4
typedef long unsigned int size_t;
# 34 "/usr/include/stdio.h" 2 3 4

# 1 "/usr/include/bits/types.h" 1 3 4
# 27 "/usr/include/bits/types.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 28 "/usr/include/bits/types.h" 2 3 4


typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;


typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;

typedef signed long int __int64_t;
typedef unsigned long int __uint64_t;

---------------省略一大部分内容------------------------------


---------------省略一大部分内容------------------------------

# 2 "test.c" 2

int main()
{
puts("hello, it,s me!");
return 0;
}


编译阶段

词法分析,语法分析,将源文件代码转为中间汇编代码

gcc -S test.i -o test.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

.file "test.c"
.section .rodata
.LC0:
.string "hello, it,s me!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)"
.section .note.GNU-stack,"",@progbits

汇编阶段

将汇编代码翻译为目标代码(机器代码),即二进制文件

gcc -c test.s -o test.o    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: 7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF............
00000010: 01 00 3E 00 01 00 00 00 00 00 00 00 00 00 00 00 ..>.............
00000020: 00 00 00 00 00 00 00 00 38 01 00 00 00 00 00 00 ........8.......
00000030: 00 00 00 00 40 00 00 00 00 00 40 00 0D 00 0A 00 ....@.....@.....
00000040: 55 48 89 E5 BF 00 00 00 00 E8 00 00 00 00 B8 00 UH.e?....h....8.
00000050: 00 00 00 5D C3 00 00 00 68 65 6C 6C 6F 2C 20 69 ...]C...hello,.i
00000060: 74 2C 73 20 6D 65 21 00 00 47 43 43 3A 20 28 47 t,s.me!..GCC:.(G
00000070: 4E 55 29 20 34 2E 38 2E 35 20 32 30 31 35 30 36 NU).4.8.5.201506
00000080: 32 33 20 28 52 65 64 20 48 61 74 20 34 2E 38 2E 23.(Red.Hat.4.8.
00000090: 35 2D 33 36 29 00 00 00 14 00 00 00 00 00 00 00 5-36)...........
000000a0: 01 7A 52 00 01 78 10 01 1B 0C 07 08 90 01 00 00 .zR..x..........
000000b0: 1C 00 00 00 1C 00 00 00 00 00 00 00 15 00 00 00 ................
000000c0: 00 41 0E 10 86 02 43 0D 06 50 0C 07 08 00 00 00 .A....C..P......
000000d0: 00 2E 73 79 6D 74 61 62 00 2E 73 74 72 74 61 62 ..symtab..strtab
000000e0: 00 2E 73 68 73 74 72 74 61 62 00 2E 72 65 6C 61 ..shstrtab..rela
000000f0: 2E 74 65 78 74 00 2E 64 61 74 61 00 2E 62 73 73 .text..data..bss
00000100: 00 2E 72 6F 64 61 74 61 00 2E 63 6F 6D 6D 65 6E ..rodata..commen
00000110: 74 00 2E 6E 6F 74 65 2E 47 4E 55 2D 73 74 61 63 t..note.GNU-stac
00000120: 6B 00 2E 72 65 6C 61 2E 65 68 5F 66 72 61 6D 65 k..rela.eh_frame
-----------------------省略下面的内容----------------------------------
*/

链接阶段

重定位是将符号引用与符号定义进行链接的过程。因此链接是处理可重定位文件,把它们的各种符号引用和符号定义转换为可执行文件中的合适信息(一般是虚拟内存地址)的过程。

链接又分为静态链接和动态链接,前者是程序开发阶段程序员用 ld(gcc 实际上在后台调用了 ld)静态链接器手动链接的过程,而动态链接则是程序运行期间系统调用动态链接器(ld-linux.so)自动链接的过程。

gcc test.o -o test

[root@localhost test]# ldd test
linux-vdso.so.1 =>  (0x00007fff22f44000)
libc.so.6 => /lib64/libc.so.6 (0x00007f80760ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f80764c2000)

函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。

ending

tumblr_ph8vgsw3hl1sfie3io1_1280.jpg

----------- ending -----------