-/*******************************************************************************************************************************+++++++++++++/
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L021415
班 级 2003001
学 生 申吴鑫
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本文通过研究hello.c在linux中的各类操作,从预处理开始,经过编译、汇编、链接等一系列过程,了解其在不同操作后的变化,同时了解了linux环境下计算机内进程管理、存储管理以及IO管理的方式,全方位多层次得了解了计算机的工作模式与工作结构。
关键词:预处理、编译、汇编、链接、进程管理、存储管理、IO管理;
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
5.3 可执行目标文件hello的格式..................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程...................................................... - 10 -
6.3 Hello的fork进程创建过程...................................................................... - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................. - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理....................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换.............................................. - 11 -
7.5 三级Cache支持下的物理内存访问........................................................... - 11 -
7.6 hello进程fork时的内存映射................................................................... - 11 -
7.7 hello进程execve时的内存映射............................................................... - 11 -
7.8 缺页故障与缺页中断处理........................................................................... - 11 -
8.1 Linux的IO设备管理方法........................................................................... - 13 -
8.2 简述Unix IO接口及其函数........................................................................ - 13 -
第1章 概述
1.1 Hello简介
hello程序的生命周期是从一个高级C语言程序开始的,因为这种形式能够被人读懂。然而,为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级 机器语言指令。然后这些指令按照一种称为“可执行目标程序”的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称为 可执行目标文件。
1)P2P:即Program to Progress,Hello.c在一开始只是C语言形式的文本文件,再经过CPP预处理器处理过后生成hello.i(修改了的源程序,文本),然后经过ccl编译器生成hello.s(汇编程序,文本),接着经过汇编器(as)生成hello.o(可重定位目标程序,二进制),最终经过链接器(ld)生成hello(可执行目标程序,二进制)。
2)020:From Zero to Zero,刚开始内存中是没有程序信息的,程序需要加载到内存中才能运行,运行可执行文件时, hello会被分配一个虚拟内存,对应着一个物理内存,里面存放着程序需要运行的信息。之后便可以执行程序,CPU和系统通过上下文切换,进程管理,处理所有的汇编指令。最终hello程序返回,会被shell回收,对应内存和虚拟内存随即释放,结束运行。
1.2 环境与工具
软件环境:Windows10 64位;Vmware 16.0;Ubuntu 20.04.
使用工具:Codeblocks; Objdump; edb-debugger; Hexedit.
1.3 中间结果
(1)hello.c:给定的C语言源文件。
(2)hello.i:.C文件预处理生成的预处理文件。
(3)hello.s:汇编代码文件。
(4)hello.o:可重定位目标文件,属于ELF格式,可查看ELF信息结构。
(5)hello:可执行文件,属于ELF文件格式,可查看ELF信息结构。
1.4 本章小结
计算机系统是由硬件和系统软件组成的,它们共同协作以运行应用程序。计算机内部的信息被表示为一组组的位,它们依据上下文有不同的解释方式。程序被其他程序翻译成不同的形式,开始时是ASCII文本,然后被编译器和链接器翻译成二进制可执行文件。
第2章 预处理
2.1 预处理的概念与作用
在本节中,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。
1)概念
预处理过程是整个编译过程的第一步。以C语言为例,预处理的输入文件是c文件和h文件,输出文件是i文件。
在预处理过程中,预处理器(Preprocessor)会分析预处理指令(包括#include头文件和#define宏定义等)以及去除源代码中的注释。
通常来说,编译器会直接将c文件编译成o文件,然后再去交给链接器链接为elf文件。如果希望通过命令行仅仅将c文件预处理成为i文件,只要在命令行中加上-E参数,就可以使编译器在预处理后停止下来,输出预处理后的源文件。
2)作用
1. 预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的。程序员在程序中用预处理命令来调用这些功能。
2. 文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
3. 条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
4. 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
2.2在Ubuntu下预处理的命令
相比于一开始的hello.c文件,hello.i文件的长度是其数百倍,原因在于预处理过后的文本文件会对源文件中的各类声明进行替换,具体原则如下:
一.预定义符号
主要有:
__FILE__ //表示进行编译的文件名
__LINE__ //表示文件当前的行号
__DATE__ //表示文件被编译的日期
__TIME__ //表示文件被编译的时间
__STDC__ //如果编译器遵循ANSI.C,其值为1,否则未定义
二.#define
1.#define定义标识符
2.#define定义宏
三.#undef
这条指令可以移除一个宏定义,Ex:#undef name,移除name的宏定义。
四.条件编译
五.文件包含
2.3 Hello的预处理结果解析
截取部分我们可以看到新生成的.i文件对源文件的文件头进行了大量替换。
2.4 本章小结
本章对hello.c的内容进行了预处理,并通过分析预处理后的结果得到了预处理的功能以及优势,对于计算机的编译过程有了更深层次的了解。
第3章 编译
3.1 编译的概念与作用
编译阶段。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的定义,定义中每条语句都以一种文本格式描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1 栈帧预处理
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
main函数的两个参数为argc与字符串数组argv,分别通过%edi与%rsi保存传递。
3.3.2 条件判断
cmpl $4, -20(%rbp)
此处为将参数argc与4进行比较,判断用户键入参数个数是否为4。
cmpl $7, -4(%rbp)
此处为将循环变量i与7进行比较,判断程序是否满足继续循环的条件。
3.3.3控制转移
je .L2
此处为在满足argc=4的情况下跳转至.L2处继续该程序
jmp .L3
此处为程序中直接的跳转程序,跳转至.L3处
jle .L4
此处为满足循环变量i<=7时继续循环。
3.3.4赋值及算数操作
movl $0, -4(%rbp)
定义i=0并将其存入栈帧中。
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
此处应用了多个赋值以及加法操作,根据栈帧预处理我们知道argv中的字符存储在寄存器%rbp偏移32位的地址中,movq -32(%rbp), %rax,首先将存储字符串的起点地址保存在%rax当中,接着考虑到一个char类型的变量占据8位大小,%rax加上16可得到argv[2]中的字符,将argv[2]中的字符保存至%rdx当中。movq -32(%rbp), %rax
addq $8, %rax,这两行命令可以索引到argv[1]中的字符,movq %rax, %rsi
再将其存入寄存器%rsi当中。
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
这里的几步操作与上述操作相类似,索引到argv[3]中的值,经过atoi处理后调用sleep函数实现。
addl $1, -4(%rbp)
此处位对循环变量i的处理,i++。
3.3.5函数操作
Main函数中调用了puts、exit、getchar、print、atoi、sleep等函数。
3.4 本章小结
本章通过对汇编语言的分析,具体入微地了解了编译器的工作细节,从数据处理、函数操作、控制转移等各方面了解了计算机处理程序的方式,帮助我更好地理解我书写的程序在计算机内部的工作方式,从而了解并且提高工作效率。
第4章 汇编
4.1 汇编的概念与作用
汇编阶段:接下来,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定向目标程序的格式,并将结果保存在目标文件hello.o当中。hello.o文件是一个二进制文件,它包含的17个字节是函数main的指令编码。
汇编的作用:将汇编代码转换为机器指令,使其在链接后能被机器识别并执行。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
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: 1240 (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: 14
Section header string table index: 13
首先分析ELF Header中的信息,在此文件中,可看到其类型为REL,即可重定向文件,其中section头的个数为14,下面是14个Section头的详细信息:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000092 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000388
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000d2
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000d8
0000000000000033 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 0000010b
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00000137
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000138
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000158
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000448
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000190
00000000000001b0 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000340
0000000000000048 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000460
0000000000000074 0000000000000000 0 0 1
其中Addr在此处被填充为了0的原因是,其目前并不需要被加载到内存中,在链接的时候才会被填充。
包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。以hello.s为例,节头部表一共描述了13个不同节的位置、大小等信息。
1.text节,大小为0x92,类型为PROGBITS,标志为AX(只读可执行),偏移量为0X40,
2.rela.text节,大小为0xc0,类型为RELA,标志为I,偏移0x388 字节
3.data节,已初始化的全局变量节,类型为PROGBITS,标志为WA(可读写),偏移0xd2
4.bss节,未初始化的全局和静态变量或初始化为0的全局或静态变量,大小为0,类型为PROGBITS,标志为WA,偏移0xd2
5.rodata节,只读数据,大小为0x33,标志为A(只读),偏移0xd8
6.comment节,版本控制信息,大小为0x2c,标志为MS,偏移为0x10b
7.note.GNU_stack节:标记可执行堆栈,大小为0x0字节,类型为PROGBITS,偏移量为0x137
8. .note.gnu.propert,类型为NOTE,大小0x0字节,偏移量为0x138.
9.eh_frame节:处理异常,大小为0x38字节,类型为PROGBITS,偏移量为0x158,标志为A(表明该节的数据只读)。
10.rela.eh_frame节:.eh_frame节的重定位信息,大小为0x18字节,类型为RELA,偏移量为0x448,标志为I。
11.symtab节:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。大小为0x18字节,类型为SYMTAB,偏移量为0x190
12.strtab节:一个字符串表,包括.symtab和.debug节中的符号表,以及节头,偏移量为0x340字节
13.shstrtab节:大小为0x74字节,也是一个字符串表,偏移量为0x460字节
重定位节
Relocation section '.rela.text' at offset 0x388 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001c 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000021 000c00000004 R_X86_64_PLT32 0000000000000000 puts - 4
00000000002b 000d00000004 R_X86_64_PLT32 0000000000000000 exit - 4
000000000054 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 22
00000000005e 000e00000004 R_X86_64_PLT32 0000000000000000 printf - 4
000000000071 000f00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000078 001000000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000087 001100000004 R_X86_64_PLT32 0000000000000000 getchar – 4
当汇编器生成一个目标模块时,它并不知道数据和代码最终将存放在存储器中的什么位置。它也不知道这个模块引用的任何外部定义的函数和全局变量。所以,无论何时汇编器遇到对最终位置未指定目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并可执行文件时如何修改这个引用。代码重定位条目放在.rel.text中。已经初始化数据的重定位条目放在.rel.data中。重定位条目中描述了需重定位符号的偏移量、信息、类型、符号值及名称等。
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 146 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar
存放变量信息。
4.4 Hello.o的结果解析
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 20 sub $0x20,%rsp
c: 89 7d ec mov %edi,-0x14(%rbp)
f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
17: 74 16 je 2f <main+0x2f>
19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
1c: R_X86_64_PC32 .rodata-0x4
20: e8 00 00 00 00 callq 25 <main+0x25>
21: R_X86_64_PLT32 puts-0x4
25: bf 01 00 00 00 mov $0x1,%edi
2a: e8 00 00 00 00 callq 2f <main+0x2f>
2b: R_X86_64_PLT32 exit-0x4
2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
36: eb 48 jmp 80 <main+0x80>
38: 48 8b 45 e0 mov -0x20(%rbp),%rax
3c: 48 83 c0 10 add $0x10,%rax
40: 48 8b 10 mov (%rax),%rdx
43: 48 8b 45 e0 mov -0x20(%rbp),%rax
47: 48 83 c0 08 add $0x8,%rax
4b: 48 8b 00 mov (%rax),%rax
4e: 48 89 c6 mov %rax,%rsi
51: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 58 <main+0x58>
54: R_X86_64_PC32 .rodata+0x22
58: b8 00 00 00 00 mov $0x0,%eax
5d: e8 00 00 00 00 callq 62 <main+0x62>
5e: R_X86_64_PLT32 printf-0x4
62: 48 8b 45 e0 mov -0x20(%rbp),%rax
66: 48 83 c0 18 add $0x18,%rax
6a: 48 8b 00 mov (%rax),%rax
6d: 48 89 c7 mov %rax,%rdi
70: e8 00 00 00 00 callq 75 <main+0x75>
71: R_X86_64_PLT32 atoi-0x4
75: 89 c7 mov %eax,%edi
77: e8 00 00 00 00 callq 7c <main+0x7c>
78: R_X86_64_PLT32 sleep-0x4
7c: 83 45 fc 01 addl $0x1,-0x4(%rbp)
80: 83 7d fc 07 cmpl $0x7,-0x4(%rbp)
84: 7e b2 jle 38 <main+0x38>
86: e8 00 00 00 00 callq 8b <main+0x8b>
87: R_X86_64_PLT32 getchar-0x4
8b: b8 00 00 00 00 mov $0x0,%eax
90: c9 leaveq
91: c3 retq
该形式的汇编代码与hello.s差别较小,主要几个差别体现在:
1)由L1、L2、L3表示的跳转标志更改为固定的地址偏移量,原因在于经过汇编操作后,汇编器为hello.s文件分配了指定的逻辑地址,因此可以采用固定偏移量进行表示。
2)全局变量的访问不能直接访问,需要访问.rodata文件中的内容,这个过程在重定位之前和函数一样,不能确定访问的地址,所以暂时仍然使用000000作为地址。
4.5 本章小结
了解了ELF文件的内容,比较了重定向前后汇编程序的差别,了解了汇编器的工作任务以及作用。
第5章 链接
5.1 链接的概念与作用
链接阶段:hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数。Printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中,链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行。
概念:链接(linking)是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是在由应用程序来执行。
5.2 在Ubuntu下链接的命令
命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4010f0
Start of program headers: 64 (bytes into file)
Start of section headers: 14208 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
可以看到hello.elf中Section headers的个数变为了27个,可以体现出链接后与原elf文本文件的变化。
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000400300 00000300
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.ABI-tag NOTE 0000000000400320 00000320
0000000000000020 0000000000000000 A 0 0 4
[ 4] .hash HASH 0000000000400340 00000340
0000000000000038 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 0000000000400378 00000378
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000400398 00000398
00000000000000d8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400470 00000470
000000000000005c 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000004004cc 000004cc
0000000000000012 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000400500 00000500
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000400530 00000530
0000000000000090 0000000000000018 AI 6 21 8
[12] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000070 0000000000000010 AX 0 0 16
[14] .plt.sec PROGBITS 0000000000401090 00001090
0000000000000060 0000000000000010 AX 0 0 16
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 0000000000401238 00001238
000000000000000d 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000402000 00002000
000000000000003b 0000000000000000 A 0 0 8
[18] .eh_frame PROGBITS 0000000000402040 00002040
00000000000000fc 0000000000000000 A 0 0 8
[19] .dynamic DYNAMIC 0000000000403e50 00002e50
00000000000001a0 0000000000000010 WA 7 0 8
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000404048 00003048
0000000000000004 0000000000000000 WA 0 0 1
[23] .comment PROGBITS 0000000000000000 0000304c
000000000000002b 0000000000000001 MS 0 0 1
[24] .symtab SYMTAB 0000000000000000 00003078
00000000000004c8 0000000000000018 25 30 8
[25] .strtab STRTAB 0000000000000000 00003540
0000000000000158 0000000000000000 0 0 1
[26] .shstrtab STRTAB 0000000000000000 00003698
00000000000000e1 0000000000000000 0 0 1
区别段:
1).interp:动态链接器在操作系统中的位置不是由系统配置决定,也不是由环境参数指定,而是由 ELF 文件中的 .interp 段指定。该段里保存的是一个字符串,这个字符串就是可执行文件所需要的动态链接器的位置,常位于 /lib/ld-linux.so.2。(通常是软链接)
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000004002e0 0x00000000004002e0
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000005c0 0x00000000000005c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000245 0x0000000000000245 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x000000000000013c 0x000000000000013c R 0x1000
LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001fc 0x00000000000001fc RW 0x1000
DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001a0 0x00000000000001a0 RW 0x8
NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000320 0x0000000000400320 0x0000000000400320
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001b0 0x00000000000001b0 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.sec .text .fini
04 .rodata .eh_frame
05 .dynamic .got .got.plt .data
06 .dynamic
07 .note.gnu.property
08 .note.ABI-tag
09 .note.gnu.property
10
11 .dynamic .got
多出来的Program Header Table描述了十个Segment的信息。以.interp段为例,.interp段和前面的ELFHeader、Program Header Table一起组成一个Segment(FileSiz指出总长度为0x2e0),VirtAddr列指出第一个Segment加载到虚拟地址0x00000000004002e0(注意在x86平台上后面的PhysAddr列是没有意义的)。Flg列指出第一个Segment的访问权限是可读的。最后一列Align的值0x1(4K)是x64平台的内存页面大小。在加载时要求文件中的一页对应内存中的一页。
Dynamic section如果目标文件参与动态链接,则其程序头表将包含一个类型为 PT_DYNAMIC 的元素。此段包含 .dynamic 节。特殊符号 _DYNAMIC 用于标记包含以下结构的数组的节:
Dynamic section at offset 0x2e50 contains 21 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x401000
0x000000000000000d (FINI) 0x401238
0x0000000000000004 (HASH) 0x400340
0x000000006ffffef5 (GNU_HASH) 0x400378
0x0000000000000005 (STRTAB) 0x400470
0x0000000000000006 (SYMTAB) 0x400398
0x000000000000000a (STRSZ) 92 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x404000
0x0000000000000002 (PLTRELSZ) 144 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400530
0x0000000000000007 (RELA) 0x400500
0x0000000000000008 (RELASZ) 48 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x4004e0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x4004cc
0x0000000000000000 (NULL) 0x0
在hello 中,原来的.rela.text节已经没有了,说明链接的过程已经完成了对.rela.text的重定位操作。Hello中出现了6个新的重定位条目。这些重定位条目都和共享库中的函数有关,因为此时还没有进行动态链接,共享库中函数的确切地址仍是未知的,因此仍然需要重定位节,在动态链接后才能确定地址。
Relocation section '.rela.plt' at offset 0x530 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000404018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000404020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000404028 000400000007 R_X86_64_JUMP_SLO 0000000000000000 getchar@GLIBC_2.2.5 + 0
000000404030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 atoi@GLIBC_2.2.5 + 0
000000404038 000700000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0
000000404040 000800000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
变量存储如下:
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@GLIBC_2.2.5 (2)
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 51 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000004002e0 0 SECTION LOCAL DEFAULT 1
2: 0000000000400300 0 SECTION LOCAL DEFAULT 2
3: 0000000000400320 0 SECTION LOCAL DEFAULT 3
4: 0000000000400340 0 SECTION LOCAL DEFAULT 4
5: 0000000000400378 0 SECTION LOCAL DEFAULT 5
6: 0000000000400398 0 SECTION LOCAL DEFAULT 6
7: 0000000000400470 0 SECTION LOCAL DEFAULT 7
8: 00000000004004cc 0 SECTION LOCAL DEFAULT 8
9: 00000000004004e0 0 SECTION LOCAL DEFAULT 9
10: 0000000000400500 0 SECTION LOCAL DEFAULT 10
11: 0000000000400530 0 SECTION LOCAL DEFAULT 11
12: 0000000000401000 0 SECTION LOCAL DEFAULT 12
13: 0000000000401020 0 SECTION LOCAL DEFAULT 13
14: 0000000000401090 0 SECTION LOCAL DEFAULT 14
15: 00000000004010f0 0 SECTION LOCAL DEFAULT 15
16: 0000000000401238 0 SECTION LOCAL DEFAULT 16
17: 0000000000402000 0 SECTION LOCAL DEFAULT 17
18: 0000000000402040 0 SECTION LOCAL DEFAULT 18
19: 0000000000403e50 0 SECTION LOCAL DEFAULT 19
20: 0000000000403ff0 0 SECTION LOCAL DEFAULT 20
21: 0000000000404000 0 SECTION LOCAL DEFAULT 21
22: 0000000000404048 0 SECTION LOCAL DEFAULT 22
23: 0000000000000000 0 SECTION LOCAL DEFAULT 23
24: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
25: 0000000000000000 0 FILE LOCAL DEFAULT ABS
26: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
27: 0000000000403e50 0 OBJECT LOCAL DEFAULT 19 _DYNAMIC
28: 0000000000403e50 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
29: 0000000000404000 0 OBJECT LOCAL DEFAULT 21 _GLOBAL_OFFSET_TABLE_
30: 0000000000401230 5 FUNC GLOBAL DEFAULT 15 __libc_csu_fini
31: 0000000000404048 0 NOTYPE WEAK DEFAULT 22 data_start
32: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
33: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 _edata
34: 0000000000401238 0 FUNC GLOBAL HIDDEN 16 _fini
35: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2.5
36: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
37: 0000000000404048 0 NOTYPE GLOBAL DEFAULT 22 __data_start
38: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getchar@@GLIBC_2.2.5
39: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
40: 0000000000402000 4 OBJECT GLOBAL DEFAULT 17 _IO_stdin_used
41: 00000000004011c0 101 FUNC GLOBAL DEFAULT 15 __libc_csu_init
42: 0000000000404050 0 NOTYPE GLOBAL DEFAULT 22 _end
43: 0000000000401120 5 FUNC GLOBAL HIDDEN 15 _dl_relocate_static_pie
44: 00000000004010f0 47 FUNC GLOBAL DEFAULT 15 _start
45: 000000000040404c 0 NOTYPE GLOBAL DEFAULT 22 __bss_start
46: 0000000000401125 146 FUNC GLOBAL DEFAULT 15 main
47: 0000000000000000 0 FUNC GLOBAL DEFAULT UND atoi@@GLIBC_2.2.5
48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5
49: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@@GLIBC_2.2.5
50: 0000000000401000 0 FUNC GLOBAL HIDDEN 12 _init
5.4 hello的虚拟地址空间
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
以.text段为例,该段起始地址为00000000004010f0,
与在edb中的查询结果相匹配,符合段地址的描述。
5.5 链接的重定位过程分析
经过重定位后的汇编代码与重定位前主要有以下几点差别:
1.重定位后的汇编代码使用虚拟内存来代替重定位前的标记(L1、L2)。
2.重定位后代码中将会把其他各类库函数信息进行标注并体现。
5.6 hello的执行流程
跳转过程如下:
ld-2.27.so!_dl_start -> ld-2.27.so!_dl_init -> hello!_start -> ld-2.27.so!_dl_start_main -> ld-2.27.so!_cxa_atexit -> hello!_libc_csu_init -> libc-2.27.so!setjump -> hello!printf@plt -> hello!atoi@plt -> hello!sleep@plt -> hello!getchar@plt - >hello!exit@plt
5.7 Hello的动态链接分析
在运行dl_start和dl_init之前,GOTPLT表的内容如图所示:
在运行dl_start和dl_init之后,GOTPLT表的内容如图所示:
动态链接的基本思想就是把程序按照模块拆分成各个相对独立部分,在程序运行时才将他们链接在一起形成一个完成的程序,而不是像静态链接把所有的模块都链接成一个单独的可执行文件。在Linux下ELF动态链接被称为动态共享库(DSO)。
动态链接的好处:
1.动态链接将共享对象放置在内存中,不仅仅节省内存,它还可以减少物理页面的换进换出,也可以提高CPU缓存的命中率,因为不同进程间的数据与指令都集中在同一个共享模块上。
2.当一个软件模块发生改变的时,只需要覆盖需要更新的文件,等程序下一次运行时自动链接更新那么,就算是跟新完成了。
3.增加程序的可扩展性和兼容性,它可以在运行时动态的加载各种程序模块,就是后来的插件(plug-in).
5.8 本章小结
本章介绍了重定位的过程,以及经过重定位后生成的ELF文件与重定位前的区别,了解了可执行文件在计算机内的执行流程,了解了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程的经典定义是一个执行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
进程的作用:向用户提供了一种假象。 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序。
其基本功能是解释并运行用户的指令,重复如下处理过程:
1.终端进程读取用户由键盘输入的命令行。
2.分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
3.检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
4.如果不是内部命令,调用fork( )创建新进程/子进程
5.在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
6.如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait)等待作业终止后返回。
7.如果用户要求后台运行(如果命令末尾有&号),则shell返回。
6.3 Hello的fork进程创建过程
父进程可以通过fork函数创建一个新的运行的子进程,其函数声明为:
pid_t fork(void),子进程享有与父进程相同但各自独立的上下文,包括代码、堆、数据段、共享库以及用户栈。
在父进程中,fork函数返回子进程的PID,在子进程中,fork函数返回0.
当我们在终端中输入./hello时,shell会先判断发现这个参数并不是Shell内置的命令,于是就把这条命令当作一个可执行程序的名字,它的判断显然是对的。接下了shell会执行fork函数为hello创建进程。
6.4 Hello的execve过程
首先execve函数利用参数名,调用函数hello,能取得对应文件的i节点,然后将当前进程(子进程)的i节点置换为上面操作得到的节点,释放内存页表并修改为LDT。
具体操作如下:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
上下文切换: 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
开始hello运行在用户模式,收到信号后进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。
6.6 hello的异常与信号处理
1.程序正常执行
2.通过Ctrl+Z挂起程序,输入ps查看进程:
3.jobs查看当前作业
、
4.fg运行前台程序
5.调用pstree查看进程树
systemd─┬─ModemManager───2*[{ModemManager}]
├─NetworkManager───2*[{NetworkManager}]
├─3*[VBoxClient───VBoxClient───2*[{VBoxClient}]]
├─VBoxClient───VBoxClient───3*[{VBoxClient}]
├─VBoxService───8*[{VBoxService}]
├─accounts-daemon───2*[{accounts-daemon}]
├─acpid
├─avahi-daemon───avahi-daemon
├─colord───2*[{colord}]
├─cron
├─cups-browsed───2*[{cups-browsed}]
├─cupsd
├─dbus-daemon
├─gdm3─┬─gdm-session-wor─┬─gdm-x-session─┬─Xorg───{Xorg}
│ │ │ ├─gnome-session-b─┬─ssh-agent
│ │ │ │ └─2*[{gnome-+
│ │ │ └─2*[{gdm-x-session}]
│ │ └─2*[{gdm-session-wor}]
│ └─2*[{gdm3}]
├─gnome-keyring-d─┬─ssh-agent
│ └─3*[{gnome-keyring-d}]
├─irqbalance───{irqbalance}
├─2*[kerneloops]
├─networkd-dispat
├─polkitd───2*[{polkitd}]
├─rsyslogd───3*[{rsyslogd}]
├─rtkit-daemon───2*[{rtkit-daemon}]
├─snapd───10*[{snapd}]
├─switcheroo-cont───2*[{switcheroo-cont}]
├─systemd─┬─(sd-pam)
│ ├─at-spi-bus-laun─┬─dbus-daemon
│ │ └─3*[{at-spi-bus-laun}]
│ ├─at-spi2-registr───2*[{at-spi2-registr}]
│ ├─dbus-daemon
│ ├─dconf-service───2*[{dconf-service}]
│ ├─evolution-addre───5*[{evolution-addre}]
│ ├─evolution-calen───8*[{evolution-calen}]
│ ├─evolution-sourc───3*[{evolution-sourc}]
│ ├─gjs───4*[{gjs}]
│ ├─gnome-session-b─┬─evolution-alarm───5*[{evolution-alarm}]
│ │ ├─gsd-disk-utilit───2*[{gsd-disk-utilit}]
│ │ ├─update-notifier───3*[{update-notifier}]
│ │ └─3*[{gnome-session-b}]
│ ├─gnome-session-c───{gnome-session-c}
│ ├─gnome-shell─┬─ibus-daemon─┬─ibus-dconf───3*[{ibus-dconf}]
│ │ │ ├─ibus-engine-lib───3*[{ibus-engi+
│ │ │ ├─ibus-engine-sim───2*[{ibus-engi+
│ │ │ ├─ibus-extension-───3*[{ibus-exte+
│ │ │ └─2*[{ibus-daemon}]
│ │ └─6*[{gnome-shell}]
│ ├─gnome-shell-cal───5*[{gnome-shell-cal}]
│ ├─gnome-terminal-─┬─bash───pstree
│ │ └─4*[{gnome-terminal-}]
│ ├─goa-daemon───3*[{goa-daemon}]
│ ├─goa-identity-se───2*[{goa-identity-se}]
│ ├─gsd-a11y-settin───3*[{gsd-a11y-settin}]
│ ├─gsd-color───3*[{gsd-color}]
│ ├─gsd-datetime───3*[{gsd-datetime}]
│ ├─gsd-housekeepin───3*[{gsd-housekeepin}]
│ ├─gsd-keyboard───3*[{gsd-keyboard}]
│ ├─gsd-media-keys───4*[{gsd-media-keys}]
│ ├─gsd-power───3*[{gsd-power}]
│ ├─gsd-print-notif───2*[{gsd-print-notif}]
│ ├─gsd-printer───2*[{gsd-printer}]
│ ├─gsd-rfkill───2*[{gsd-rfkill}]
│ ├─gsd-screensaver───2*[{gsd-screensaver}]
│ ├─gsd-sharing───3*[{gsd-sharing}]
│ ├─gsd-smartcard───4*[{gsd-smartcard}]
│ ├─gsd-sound───3*[{gsd-sound}]
│ ├─gsd-usb-protect───3*[{gsd-usb-protect}]
│ ├─gsd-wacom───2*[{gsd-wacom}]
│ ├─gsd-wwan───3*[{gsd-wwan}]
│ ├─gsd-xsettings───3*[{gsd-xsettings}]
│ ├─gvfs-afc-volume───3*[{gvfs-afc-volume}]
│ ├─gvfs-goa-volume───2*[{gvfs-goa-volume}]
│ ├─gvfs-gphoto2-vo───2*[{gvfs-gphoto2-vo}]
│ ├─gvfs-mtp-volume───2*[{gvfs-mtp-volume}]
│ ├─gvfs-udisks2-vo───3*[{gvfs-udisks2-vo}]
│ ├─gvfsd─┬─gvfsd-dnssd───2*[{gvfsd-dnssd}]
│ │ ├─gvfsd-network───3*[{gvfsd-network}]
│ │ ├─gvfsd-recent───2*[{gvfsd-recent}]
│ │ ├─gvfsd-trash───2*[{gvfsd-trash}]
│ │ └─2*[{gvfsd}]
│ ├─gvfsd-fuse───5*[{gvfsd-fuse}]
│ ├─gvfsd-metadata───2*[{gvfsd-metadata}]
│ ├─ibus-portal───2*[{ibus-portal}]
│ ├─ibus-x11───2*[{ibus-x11}]
│ ├─pulseaudio───3*[{pulseaudio}]
│ ├─snap-store───4*[{snap-store}]
│ ├─tracker-miner-f───4*[{tracker-miner-f}]
│ ├─xdg-desktop-por───4*[{xdg-desktop-por}]
│ ├─xdg-desktop-por───3*[{xdg-desktop-por}]
│ ├─xdg-document-po───5*[{xdg-document-po}]
│ └─xdg-permission-───2*[{xdg-permission-}]
├─systemd-journal
├─systemd-logind
├─systemd-resolve
├─systemd-timesyn───{systemd-timesyn}
├─systemd-udevd
├─udisksd───4*[{udisksd}]
├─unattended-upgr───{unattended-upgr}
├─upowerd───2*[{upowerd}]
├─whoopsie───2*[{whoopsie}]
└─wpa_supplicant
6.异常处理
在程序运行过程中随意键入字符对程序运行不会产生影响,无法识别的字符将被忽略,以无法解析符的形式存在。
6.7本章小结
通过本章了解了hello程序在进程中的运行方式,了解了各类进程相关的命令,以及面对异常进程计算机的处理方式。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在程序运行时由中央处理单元生成的内容的地址称为逻辑地址。该地址也称为虚拟地址。当我们谈论逻辑地址时,我们指的是CPU分配给每个进程的地址,一个进程在内存中所处的实际地址与进程认为它所处的地址是不一样的。
线性地址:线性地址(Linear Address)也叫虚拟地址(virtual address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。
虚拟地址:也即是逻辑地址。
物理地址:CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。引号,可以理解为数组的下标——而它将会对应一个数组,它又是什么的索引呢?这就是“段描述符(segment descriptor)”,段描述符具体地址描述了一个段(对于“段”这个字眼的理解:我们可以理解为把虚拟内存分为一个一个的段。比如一个存储器有1024个字节,可以把它分成4段,每段有256个字节)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,如图:
Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。选择GDT还是LDT取决于段选择符中的T1,若T1等于0则选择GDT,反之选择LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
首先,给定一个完整的逻辑地址 [段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
7.3 Hello的线性地址到物理地址的变换-页式管理
CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。
如上图
1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2、每一个进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的物理地址;
7.4 TLB与四级页表支持下的VA到PA的变换
首先将CPU内核发送过来的32位VA[31:0]分成三段,前两段VA[31:20]和VA[19:12]作为两次查表的索引,第三段VA[11:0]作为页内的偏移,查表的步骤如下:
⑴从协处理器CP15的寄存器2(TTB寄存器,translation table base register)中取出保存在其中的第一级页表(translation table)的基地址,这个基地址指的是PA,也就是说页表是直接按照这个地址保存在物理内存中的。
⑵以TTB中的内容为基地址,以VA[31:20]为索引值在一级页表中查找出一项(2^12=4096项),这个页表项(也称为一个描述符,descriptor)保存着第二级页表(coarse page table)的基地址,这同样是物理地址,也就是说第二级页表也是直接按这个地址存储在物理内存中的。
⑶以VA[19:12]为索引值在第二级页表中查出一项(2^8=256),这个表项中就保存着物理页面的基地址,我们知道虚拟内存管理是以页为单位的,一个虚拟内存的页映射到一个物理内存的页框,从这里就可以得到印证,因为查表是以页为单位来查的。
⑷有了物理页面的基地址之后,加上VA[11:0]这个偏移量(2^12=4KB)就可以取出相应地址上的数据了。
这个过程称为Translation Table Walk,Walk这个词用得非常形象。从TTB走到一级页表,又走到二级页表,又走到物理页面,一次寻址其实是三次访问物理内存。注意这个“走”的过程完全是硬件做的,每次CPU寻址时MMU就自动完成以上四步,不需要编写指令指示MMU去做,前提是操作系统要维护页表项的正确性,每次分配内存时填写相应的页表项,每次释放内存时清除相应的页表项,在必要的时候分配或释放整个页表。
7.5 三级Cache支持下的物理内存访问
处理器微架构访问Cache的方法与访问主存储器有类似之处。主存储器使用地址编码方式,微架构可以地址寻址方式访问这些存储器。Cache也使用了类似的地址编码方式,微架构也是使用这些地址操纵着各级Cache,可以将数据写入Cache,也可以从Cache中读出内容。只是这一切微架构针对Cache的操作并不是简单的地址访问操作。为简化起见,我们忽略各类Virtual Cache,讨论最基础的Cache访问操作,并借此讨论CPU如何使用TLB完成虚实地址转换,最终完成对Cache的读写操作。
Cache的存在使得CPU Core的存储器读写操作略微显得复杂。CPU Core在进行存储器方式时,首先使用EPN(Effective Page Number)进行虚实地址转换,并同时使用CLN(Cache Line Number)查找合适的Cache Block。这两个步骤可以同时进行。在使用Virtual Cache时,还可以使用虚拟地址对Cache进行寻址。在多数处理器微架构中,Cache由多行多列组成,使用CLN进行索引最终可以得到一个完整的Cache Block。但是在这个Cache Block中的数据并不一定是CPU Core所需要的。因此有必要进行一些检查,将Cache Block中存放的Address与通过虚实地址转换得到的PA进行地址比较(Compare Address)。如果结果相同而且状态位匹配,则表明Cache Hit。此时微架构再经过Byte Select and Align部件最终获得所需要的数据。如果发生Cache Miss,CPU需要使用PA进一步索引主存储器获得最终的数据。
7.6 hello进程fork时的内存映射
内存映射:Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个内存区域的内容,这个过程称为内存映射。一个虚拟页面一旦被初始化了,它就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫交换空间或者交换区域,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同,当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。
7.7 hello进程execve时的内存映射
首先execve函数利用参数名,调用函数hello,能取得对应文件的i节点,然后将当前进程(子进程)的i节点置换为上面操作得到的节点,释放内存页表并修改为LDT。
具体操作如下:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
当CPU执行指令希望访问一个不在内存的页面时,将产生缺页中断,系统开始运行中断处理程序。此时指令计数器(PC) 的值尚未来得及增加就被压入堆栈,因此压入的断点必然是本次被中断的指令地址,而非下一条指令的地址。
缺页中断处理过程
(1) 保留进程上下文
(2)判断内存是否有空闲可用帧?若有,则获取一个帧号No,转(4) 启动I/O过程。若无,继续(3)
(3)腾出一个空闲帧,即:
(3)-1调用置换算法,选择一个淘汰页PTj。
(3)-2 PTj(S)=0 ; //驻留位置0
(3)-3 No= PTj (F); //取该页帧号
(3)-4 若该页曾修改过,则
(3)-4-1 请求外存交换区上一个空闲块B ;
(3)-4-2 PTj(D)=B ;//记录外存地址
(3)-4-3启动I/O管理程序,将该页写到外存上。
(4)按页表中提供的缺页外存位置,启动I/O,将缺页装入空闲帧No中。
(5)修改页表中该页的驻留位和内存地址。PTi(S)=1 ; PTi(F) =No。
(6)结束。
7.9动态存储分配管理
动态存储分配的几个方式:
1)malloc 函数
分配指定字节数的存储区。此存储区的初始值不确定。
2)calloc 函数
为指定数量指定长度的对象分配存储空间。该空间中的每一位(bit)都初始化为 0。
3)realloc 函数
增加或减少以前分配区的长度。这里重点说一下 realloc 函数
(1) 减少存储区的长度,这个简单直接减少就行;
(2) 增加存储区长度,如果在该存储区后有足够的空间可供扩充,则可在原存储区上向高地址方向扩充,无需移动任何原先的内容;并返回与传给他相同的指针值;
(3) 如果原存储区后没有足够的空间,则 realloc 分配另一足够大的存储区,再将原来空间的元素搬移过去,然后释放原存储区,返回新分配区的指针。新的区域,初始值不确定。
注意:realloc 的最后一个参数是存储区的长度,不是新、旧存储区长度之差。
7.10本章小结
本章通过hello程序,帮助了解了其在存储器地址空间的表示方式 ,阐述了逻辑地址到线性地址,以及线性地址到物理地址之间的转化流程,同时了解了cache、TLB、fork、execve的工作原理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:unix io接口,使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会出发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处没有明确的“EOF符号”。类似的,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
函数:
打开和关闭文件:int open(char *filename, int flags, mode_t mode);
int close(int fd);
读和写文件:ssize_t read(int fd, void *buf, size_t n);
Ssize_t write(int fd, const void *buf, size_t n);
8.3 printf的实现分析
1.从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
先看paintf函数:
int printf(const char *fmt,...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt)+4);
i=vsprintf(buf,fmt,arg);
write(buf,i);
rerurn i;
}
“…”:是可变形参的一种写法,当传递参数的个数不确定时,就可以用这种方式来表示。类似于template的写法。
通过va_list arg = (va_list)((char*)(&fmt) + 4)语句找到“…”中的第一个参数。接着调用了vsprintf函数,返回打印字符串的长度。
接下来,printf函数会调用系统IO函数:write,其作用就是从缓存buf中最多读i个字节复制到一个文件位置。
2.字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
3.显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
8.4 getchar的实现分析
getchar的源代码:
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return (--n>=0)?(unsigned char)*bb++:EOF;
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
通过本章,我了解了linux环境下IO设备的管理方式,IO的函数实现以及具体IO函数的内部操作。
结论
Hello经历了如下阶段:(1) 预处理:预处理器cpp将.c文件翻译成.i的文件;(2) 编译:gcc编译器将.i文件翻译成.s格式的汇编语言文件;(3) 汇编:as汇编器将.s文件转换成十六进制机器码的.o文件;(4) 链接:ld链接器将一系列.o文件链接起来形成最终的可执行文件hello;(5) 进程创建:shell为hello程序fork一个子进程;(6) 程序运行:shell调用execve函数,映射虚拟内存,载入物理内存,进入main函数;(7) 指令执行:hello和其他进程并发地运行,CPU为其分配时间片;(8) 进程回收:shell回收子进程,系统释放该进程的数据所占的内存空间。
附件
hello.i hello的预处理结果
hello.s hello.i的汇编结果
hello.o hello.s翻译成的可重定位文件
hello 可执行文件
helloo.obj hello.o的反汇编结果
helloo.elf hello.o的ELF格式文件
hello.obj hello的反汇编结果
hello.elf hello的ELF格式文件
参考文献
【1】《深入理解计算机系统》【美】 兰德尔E.布莱恩特 大卫R.奥哈拉伦 机械工业出版社
【2】Unix-Linux 网络 IO 模型简介 CSDN
【3】动态存储空间分配、管理和释放 CSDN