程序员的自我修养

  1. 第一部分 - 你看程序跑起来了!!!
    1. 程序 .o中有什么? 程序是如何装入内存的?
    2. 装入内存后如何启动程序?
    3. 启动程序是从main开始的吗?
    4. malloc相关实现

这是大三下读的第一本书了。这本书在我准备面试之余还能零零碎碎读了一些。目前看了几章来总结一下。

实体书上面做笔记和翻阅还是挺舒服的,这里总结的话就不按照书中章节顺序了,而是按照自己选择阅读的顺序。

第一部分 - 你看程序跑起来了!!!

命令行一敲, 回车一按, 一个程序就运行起来了,这之间都做了哪些操作?

程序.o中有什么?

程序是如何装入内存的?

装入内存后如何启动程序?

启动程序是从main开始的吗?

程序 .o中有什么? 程序是如何装入内存的?

目前不考虑静态和动态链接库相关的内容.

static int static_int1 = 1;
static int static_int2;

int main()
{
const char* str = "Hello World!";
return 0;
}

上面的一小段代码, 包含了static 已初始化变量, static 未初始化变量, 以及字符串常量str, 一段main函数

然后使用objdump命令查看这个文件, 这个命令可以解析出ELF文件的头部分。

相关ELF文件的内容这里就不展开说了,ELF文件里面存在头和数据部分,头部分有一张类似下面的表,其中有偏移量等信息指向实际数据部分

[root@fish ~] objdump -h g-main  # h表示

g-main: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn # Size段的大小 FileOff段的实际数据在段内的偏移
9 .init 0000001b 0000000000400450 0000000000400450 00000450 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
10 .text 00000175 0000000000400470 0000000000400470 00000470 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .fini 0000000d 00000000004005e8 00000000004005e8 000005e8 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .rodata 0000001d 00000000004005f8 00000000004005f8 000005f8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
20 .data 00000008 0000000000601018 0000000000601018 00001018 2**2
CONTENTS, ALLOC, LOAD, DATA
21 .bss 00000008 0000000000601020 0000000000601020 00001020 2**2
ALLOC # 没有 CONTENTS ?

我删除了很多的内容, 只留下了目前我所学到的主要6段。后面内容基本根据这几段来说明

当在shell按下回车后,fork出的子进程去读取这个可执行文件的头部 这里面包含着这是个什么样的可执行文件, ELF文件就调用ELF装载程序,
sh文件就调用相关的bash来执行.

装载程序执行解析ELF的操作得到上面的表,然后根据表将实际数据装入虚拟内存

当然虚拟内存也不是立刻就全部装载进去,但为了便于解析这里先不考虑分页相关内容

虚拟内存 存在保留段, 是从0x08048000开始的 这样还能够防止 0x0这个地址对指针造成影响

然后从0x08048000开始装载 .init段 然后.text段 再之后.fini段, 之后是.rodata 接着.data.bss.

这几段分别是什么?

.init 包含着初始化相关代码 这些代码是自动加进来的 用于程序运行的初始化操作

.text 是我们自己编写的代码

.fini 则是终结相关代码

.rodata 我们看到表中的大小是1d 偏移是000005f8 我们使用hexdump来查看下这块内容

[root@fish ~] hexdump -s 0x5f8 -n 0x1d -C g-main # -s 跳过指定字节 -n 显示指定字节 -C 显示对应字符
000005f8 01 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000608 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 00 |Hello World!.|
00000615

我们看到了Hello World!\0 这段内容说明这正是全局const变量和字符串常量的内容

.data 可以看到这里存储的是static int static_int1 = 1; 这个带有初始值的static变量

[root@fish ~] hexdump -s 0x1018 -n 0x8 -C g-main 
00001018 00 00 00 00 01 00 00 00 |........|
00001020

.bss 这段实际上是没有内容的 虽然存在size和off_set 但可以看到他没有CONTENTS
这里存储的是不带初始值的static变量static int static_int2; 方便进行统一的初始化为0, 同时不占用文件大小.

这样上面相关的6段就装载完成了, 除此之外还有堆段和栈段, 最终布局如下

内存空间
内核占用3-4GB
栈段 向低地址增长
?
堆段 向高地址增长
.bss
.data
.rodata
.fini
.text
.init
从0x0x08048000开始

下面是图片对应

装入内存后如何启动程序?

装载完毕后会设定相关的内容使程序从.init段开始执行代码

启动程序是从main开始的吗?

到这里应该有个简单的答案了, 不是从这个函数开始.

.init段中的初始化代码_start函数(汇编的形式保存在/usr/lib64/crt1.o)首先执行, 程序传入的参数是保存在栈中的

_start函数会调用__libc_start_main, 实现从栈中读取设定环境变量指针,设定argc和argv. 然后调用``/usr/lib64/crtbegin.o`中的代码进行全局初始化比如static变量的设定等

然后调用main函数

返回后调用.finit段中的代码进行收尾操作, 其中就调用了``/usr/lib64/crtend.o中的代码进行全局变量的析构之类的收尾工作, 之后调用atexit设定的函数(通过链表链接的多个atexit函数退出时执行), 再之后将main的返回值传给exit`程序退出

malloc相关实现

简单来说malloc会在分配小于128KB的空间时在堆段通过brk函数上移esp寄存器中的地址实现堆段的扩大

如果大于128KB则会在上面彩图中的Memory Mapping Segment段中anonymous mappings开辟空间, 通过的是mmap函数, mmap函数将分配到的虚拟内存映射到具体的文件,这样称之为匿名空间,匿名空间则可以作为堆空间使用


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。