文章目录
程序员的自我修养 ——— 读书笔记
第一章 温故而知新
1. 基本概念
-
计算机系统分为:硬件部分和软件部分。
-
硬件部分主要是中央处理器CPU、内存和I/O控制芯片等。
-
为了协调CPU和内存和高速的图形设备,设计了北桥(Northbridge, PCI Bridge),以便于它们之间高速交换数据。
-
专门处理低速设备则设计了南桥(Southbrigde, ISA Bridge),如磁盘、USB、键盘、鼠标等设备。
-
CPU的频率受制造工艺的限制,目前CPU的频率处于4GHz。因此从CPU的个数来提高运算速度,因此诞生了对称多处理器(SMP)。由于多个CPU的使用会增大成本,因此有了多核处理器,其与SMP的差异在于缓存共享等方面存在细微的差异。
-
系统软件:用于管理计算机本身的软件称为系统软件。可以分成2部分,一是平台性的,比如:操作系统内核、驱动程序、运行库和大量的系统工具;二是用于程序开发的,如:编译器、链接器、汇编器等开发工具和开发库。
-
计算机软件体系结构可以理解为按层设计,最底下为硬件层,操作系统,应用程序编程接口,应用程序。每层之间的通信则是通过接口连接。
-
操作系统的两大主要功能:提供抽象的接口,管理硬件资源。
-
硬盘的基本存储单位为扇区(sector),每个扇区为512字节。硬盘存储大小的计算:盘片 * 磁道 * 扇区 * 512 byte。比如一个硬盘有2个盘片,每个盘面分65536磁道,每个磁道分1024个扇区,每个扇区512字节,那么该硬盘大小为128G。
-
地址空间有两种:一种是物理地址:是存在计算机中,实实在在存在的,在计算机唯一的。另一种是虚拟地址:是虚拟的,实际不存在,每个进程都是有自己独立的虚拟空间,使每个进程只能访问自己的地址空间,这样就达到了进程之间的隔离。
-
CPU发出的是虚拟地址,也就是我们程序看到的是虚拟地址,经过内存管理单元(MMU,集成在CPU内部)就会变成物理地址。
-
分段:就是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。
-
分页:把地址空间人为地分成固定大小的页,每一页的大小有硬件决定或者硬件支持多种大小的页,有操作系统选择决定页的大小。
2. 线程基础
- 线程:可以称作为轻量级进程,是程序执行流的最小单元。
- 线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。
- 通常情况,一个进程由一个或者多个线程组成,各个线程之间共享程序的内存空间(如:代码段、数据的段、堆等)及一些进程级的资源(如打开文件和信号)。
- 多线程可以互不干扰地并发执行,并共享进程的全局变量和堆的数据。
- 使用多线程的原因:(1)多线程执行可以有效利用等待的时间。(2)程序逻辑本身就要求并发操作。(3)相对于多进程应用,多线程在数据共享方面效率要高很多。
- 线程访问非常自由,可以访问进程内存中的所有数据,甚至可以访问其他线程的堆栈(需要提前知道其他线程的堆栈地址,这种是很少见的情况)。
- 线程拥有自己的私有存储空间:(1)栈;(2)线程局部存储(TLS)(比如栈中的局部变量);(3)寄存器,寄存器是执行流的基本数据。
- 线程总是“并发”进行的。当线程数量小于等于处理器数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。当线程数量大于处理器数量,线程的并发受到一些阻碍,因为此时至少有一个处理器运行多个线程。
- 在单处理器对应多个线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多个线程程序轮流执行,所以这些线程看上去是在同时进行,实际每次只执行了一小段时间。
- 不断在处理器上切换不同的线程的行为称为线程调度。
- 线程通常至少拥有3种状态:(1)运行(2)就绪,线程可以立刻运行,但CPU已经被占用了。(3)等待,无法执行。
- 线程的优先级改变的三种方式:(1)用户指定优先级。(2)根据进入等待状态的频繁程度提升或者降低优先级。(3)长时间得不到执行而被提升优先级。
3. Linux的多线程
- Linux内核中并不存在真正意义上的线程概念,Linux将所有的执行实体(无论是线程还是进程)称为任务,每一个任务类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。
- Linux上多个任务如果是共享了同一个内存空间,则这些任务构成一个进程,每个任务就是进程里的线程。
4. 线程安全
- 多线程程序处于一个多变的环境中,可访问的全局变量和堆数据随时可能被其他线程改变。
- 多个线程访问一个共享数据,可能造成严重的恶劣后果。比如:
(1)线程1:i = 1; ++i;
(2)线程2:–i;
由于两个线程共同访问i了,i的可能结果有:0,1,2。原因是"++" "–"操作在编译成汇编语言时,会生成多条指令,在多线程执行时,可能执行一半后被调度系统打断,去执行其他代码,导致i被改写。 - 同步:指一个线程在访问数据未结束的时候,其他线程不得对同一个数据进行访问。
- 同步的最常见的方法是使用锁。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,并在结束之后释放锁,在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
- 二元信号量:是最简单的一种锁,它只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。
- 当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,此后其他得所有试图获取该二元信号量得线程将会等待,直到该锁被释放。
- 对于允许多个线程并发访问的资源,多元信号量简称信号量。
- 互斥量和二元信号量的区别:
(1)相同点:资源仅同时允许被一个线程访问。
(2)不同点:二元信号量在整个系统可以被任意线程获取并释放,也就是说同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。互斥量则是要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程去释放互斥量则是无效的。 - 临界区:是比互斥量更加严格的同步手段。把临界区的锁的获取称为进入临界区,把锁的释放称为离开临界区。
- 临界区和互斥量与信号量的区别:互斥量和信号量在系统的任何进程都是可见的。即一个进程传建了互斥量和信号量,另一个进程试图去获取该锁是合法的。临界区的作用范围仅限于本进程,其他进程无法获取该锁。除了这点,临界区具有互斥量的性质。
- ** 读写锁**:读写锁有两种获取方式:共享的或独占的。读写锁的行为如下:
- 条件变量:作为一种同步手段,作用类似一个栅栏。使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。
- volatile关键字可以阻止过度优化,它起到两点作用:
(1)阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
(2)阻止编译器调整操作volatile变量的指令顺序。——该点即使volatile可以做到,但无法阻止CPU动态调度换序。 - 代码解读:
int *pStr;
pStr = new int; //包含了三个步骤:1.分配内存;2.在内存的位置调用构造函数;3.将内存的地址赋值给pStr;
- 线程的三种模型:用户现场和内核线程存在3种模型.
(1)一对一模型:用户使用的线程就唯一对应一个内核使用的线程。(反过来就不一定,因为内核线程在用户态不一定有对应的线程存在)。缺点:操作系统存在限制内核线程数量,该模式就会限制用户线程数量。在内核线程调度时,存在上下文的切换开销较大,导致用户现程执行效率低下。
(2)多对一模型:多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行。线程切换速度比一对一切换快,线程数量不受限。缺点:如果一个线程阻塞,那么所有线程都会阻塞。
(3)多对多模型:结合以上两者的优缺点。
第二章 静态链接
1. 编译过程中隐藏的细节
- 构建:编译和链接合并在一起的过程。
- 源代码到可执行文件经历4个过程:预处理、编译、汇编、链接。
1.1 预编译
- 预编译过程主要处理源代码文件中以#开始的预编译指令。
- 预编译的对象是源文件(.c/.cpp/.h/.hpp等),预编译后的产物是.i格式文件。
- 预编译的处理规则:
(1)将所有的“#defin”删除,并且展开所有的宏定义;
(2)处理所有条件编译预指令,如:“#if"、”#ifdef"、“#else"、”#endif"等。
(3)处理“#include"预编译指令,将包含的文件拆入到该预编译指令的位置。
(4)删除所有注释”//"和“/* */;
(5)添加行号和文件标识名。
(6)保留所有的#pragma编译器指令,因为编译器需要他们。 - 由于编译后的产物.i没有任何宏名,因此可以通过该文件判断宏定义和头文件是否包含正确。
- 命令:gcc -E hello.c -o hello.i
1.2 编译
- 编译过程是将预处理输出的.i文件进行一系列词法、语法、语义分析及优化后生产相应的回报代码文件(.s格式)。
- 命令:gcc -S hello.c -o hello.s
1.3 汇编
- 汇编器将汇编代码转成机器可执行的指令,每一条汇编语句几乎对用一条机器指令。
- 命令:as hello.s -o hello.o (as是汇编器)。
1.4 链接
- 将所有的目标文件(.o文件)链接成可执行文件(.out文件)。
- 命令:ld -static hello.o 等o文件。
2. 编译器做了什么
- 汇编器就是将高级语言翻译成机器语言的一个工具。
- 编译器的编译过程有6步:扫面、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
- 静态语义:指在编译期可以确定的语义。如:声明和类型的匹配、类型的转换等。
- 动态语义:指在运行期才能确定的语义。如:0作为除数是一种在运行期语义错误。
- 编译器可以分为前端和后端,前端负责产生与机器无关的中间代码,后端则是将中间代码转成目标机器代码。因此可以针对不同的平台使用同一个编译器前端和针对不同机器平台的数个后端。
- 编译器后端主要包括代码生成器和目标代码优化器。
3. 链接器
- 重定位:指重新计算各个目标地址的过程。
- 链接:将各个模块组装起来的过程就是链接。
- 链接过程包括地址和空间分配、符号决议和重定位等步骤。
第三章 目标文件
1. 目标文件的格式
- 编译器编译源代码后但未进行链接的中间文件是目标文件。(windows是.obj,Linux是.o)
- 可执行文件格式:都是COFF(Common File format)格式的变种。
(1)Windows对应PE(Portable Executable)
(2) Linux对应ELF文件(Executable Linkable Format)
2. 目标文件中的内容
- 目标文件中包含编译后的机器指令代码、数据,还包括链接时所需要的信息,如:符号表、调试信息、字符串等。这些信息的存储形式是按照“节”或者“段”的形式。
- 程序源代码编译后的机器指令经常被放在代码段里,代码段常见名字有“.code"或".text"。
- 全局变量和局部静态变量数据放在数据段。.data段。
- 未初始化的全局变量和局部静态变量放在.bss段中,其默认值是0。.bss段只是为未初始化的全局变量和静态变量预留位置而已,并没有内容,只是方便记录这些变量的大小。.bss段和.data段的区别是:data段的数据初始化是来源于文件中的初始化,而.bss段的数据是直接初始化为0。
- ELF文件内的信息:
(1)文件头,描述整个文件的属性,包括文件是否可执行、是静态还是动态链接、目标硬件或者目标操作系统等。
(2)文件头还包括一个段表,段表是描述文件中各个段的数组,是描述各个段在文件中的便宜位置和段的属性等。 - 将数据和代码分成数据段和代码段的好处:
(1)当程序被装载后,数据和指令会分别映射到两个虚存区域。数据区域对进程来说是可读写的,而指令区是只读的,因此可以将两个虚存区的权限分别设置为可读写和只读,防止程序的指令被无意或有意的改写。
(2)指令和数据被分开存放对CPU的缓存(Cache)命中率提高有好处。
(3)当存在运行多个该程序的副本时,他们的指令都是一样的,多以内存中只需要保存一份改程序的指令部分,有利于内存共享。 - 实例来分析:
/**************************************
demo.c
gcc -c demo.c
**************************************/
#include<stdio.h>
int gInitVar = 84;
int gUnInitVar;
void func1(int i)
{
printf("%d\n",i);
}
int main()
{
static int StaticVar = 85;
static int StaticVar2;
int a = 1;
int b;
func1(StaticVar + StaticVar2 + a + b);
return a;
}
(1)通过命令:gcc -c demo.c进行编译,得到产物demo.o。(-c参数表示只编译不链接)。
(2)通过命令:objdump -h demo.o ,查看demo.o的内容,内容如下:
备注:Linux还有一个工具readelf,专门解析ELF文件,可以和objdump解析出来的进行对照。参数“-h"表示把elf文件的各个段的基本信息打印出来。参数”-x"则能输出更多的信息。
(3)解读elf文件:
I)文件中有7个段名。.text是代码段,.data是数据段,.rodata是只读数据段。
II)Size列表示段的长度,File Offset表示段的相对起始的位置的偏移量。
III)每个段的第2行中的“CONTENTS",“ALLOC”,"LOAD"等表示段的属性。其中“CONTENTS"表示该段在文件中存在。.bss段没有这个属性说明该段实际在elf文件中是不存在的。
(4)通过命令:objdump -s -d demo.o 参数”-s"可以将所有段的内容以十六进制打印出来;参数“-d”可以将所有指令的段反汇编。具体内容如下:
demo.o: file format pe-i386
Contents of section .text:
0000 5589e583 ec188b45 08894424 04c70424 U......E..D$...$
0010 00000000 e8000000 0090c9c3 5589e583 ............U...
0020 e4f083ec 20e80000 0000c744 241c0100 .... ......D$...
0030 00008b15 04000000 a1000000 0001c28b ................
0040 44241c01 c28b4424 1801d089 0424e8ad D$....D$.....$..
0050 ffffff8b 44241cc9 c3909090 ....D$......
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .drectve:
0000 202d616c 69676e63 6f6d6d3a 225f6755 -aligncomm:"_gU
0010 6e496e69 74566172 222c3200 nInitVar",2.
Contents of section .rdata:
0000 25640a00 %d..
Contents of section .rdata$zzz:
0000 4743433a 20284d69 6e47572e 6f726720 GCC: (MinGW.org
0010 47434320 4275696c 642d3229 20392e32 GCC Build-2) 9.2
0020 2e300000 .0..
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 017c0801 .........zR..|..
0010 1b0c0404 88010000 1c000000 1c000000 ................
0020 04000000 1c000000 00410e08 8502420d .........A....B.
0030 0558c50c 04040000 1c000000 3c000000 .X..........<...
0040 20000000 3d000000 00410e08 8502420d ...=....A....B.
0050 0579c50c 04040000 .y......
Disassembly of section .text:
00000000 <_func1>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 8b 45 08 mov 0x8(%ebp),%eax
9: 89 44 24 04 mov %eax,0x4(%esp)
d: c7 04 24 00 00 00 00 movl $0x0,(%esp)
14: e8 00 00 00 00 call 19 <_func1+0x19>
19: 90 nop
1a: c9 leave
1b: c3 ret
0000001c <_main>:
1c: 55 push %ebp
1d: 89 e5 mov %esp,%ebp
1f: 83 e4 f0 and $0xfffffff0,%esp
22: 83 ec 20 sub $0x20,%esp
25: e8 00 00 00 00 call 2a <_main+0xe>
2a: c7 44 24 1c 01 00 00 movl $0x1,0x1c(%esp)
31: 00
32: 8b 15 04 00 00 00 mov 0x4,%edx
38: a1 00 00 00 00 mov 0x0,%eax
3d: 01 c2 add %eax,%edx
3f: 8b 44 24 1c mov 0x1c(%esp),%eax
43: 01 c2 add %eax,%edx
45: 8b 44 24 18 mov 0x18(%esp),%eax
49: 01 d0 add %edx,%eax
4b: 89 04 24 mov %eax,(%esp)
4e: e8 ad ff ff ff call 0 <_func1>
53: 8b 44 24 1c mov 0x1c(%esp),%eax
57: c9 leave
58: c3 ret
59: 90 nop
5a: 90 nop
5b: 90 nop
I)解读:Contents of section .text:就是代码段的内容,可以结合下面的反汇编看。总共大小为0x5B。
II).data段保存的是已经初始化的全局变量和局部静态变量。实例中的gInitVar 和StaticVar 对应Contents of section .data中的54000000,55000000(存在大小端问题,实际值为0x54,0x55)。对应Size也是8Byte。
III).rdata段存放只读数据,一般是程序里的const修饰的变量和字符串常量,如printf函数中的%d。对应 Size为4Byte。
IV).bss段存放的是未初始化的全局变量和局部静态变量。gUnInitVar和StaticVar2,其段的Size是4Byte,与实际的2个变量8Byte不符合,原因是只有StaticVar2放在.bss段中,而gUnInitVar未初始化的全局变量没被放在任何段中,只是一个未定义的"COMMON”符号,需要在链接成可执行文件时才在.bss段中分配空间。
V)注意以下情况:
static int X = 0; //放在.bss段中,原因是初始化为0和局部静态变量本身会自动初始化为0,因此会被优化放在.bss段中,以节省磁盘大小。
static int y = 1; //放在.data段中
VI)ELF文件中常见的段:
6. ELF文件中的自定义段名不能使用“.”作为前缀,以防和系统的保留段名冲突。ELF文件中可以用用几个相同段名的段。
7. 自定义段名的语法格式如下:(有以下两种方式,L1D和L2D为自己定义的段名)。
#include<stdio.h>
int a
__attribute__((section ("L1D"))) = 5;
__attribute__((section ("L2D")))
int b = 10;
void main()
{ }
3. ELF文件结构
- 基本结构同下图:
- ELF目标文件最前部是ELF文件头,包含了整个文件的基本属性(如:文件版本,目标机型,程序入口地址等)。
- 段表:描述了ELF文件包含的所有段的信息。(如:段名、段长、文件偏移、读写权限、其他属性)。
- objdump -h 命令只是将段表中的主要段名显示出来,很多辅助性的段会省略,如:符号表、字符串表、重定位表等,可以通过readelf工具来查看ELF文件的段。
- 重定位表:链接器在处理目标文件时,需要对目标文件进行某些部位重定位,即代码段和数据段中那些对绝对地址的引用的位置,这些重定位的信息就记录在ELF文件的重定位表中。
- 字符串表:.strtab,用来保存普通的字符串。
- 段表字符串表:.shstrtab,用来保存段表中用到的字符串,如:段名。
4. 链接的接口——符号
- 链接过程的本质就是将多个不同的目标文件之间相互“粘”在一起。这个过程实际是目标文件之间对地址的引用,即对函数和变量的地址的引用。
- 在链接过程中,函数和变量称为是符号,函数名和变量是符号名。
- 每个定义的符号有一个对应的值,称为符号值。对变量和函数来说其符号值就是他们的地址。
- 可以通过命令:nm demo.o来查看目标文件demo.o的符号。实例如下:
00000000 b .bss
00000000 d .data
00000000 i .drectve
00000000 r .eh_frame
00000000 r .rdata
00000000 r .rdata$zzz
00000000 t .text
U ___main
00000000 T _func1
00000000 D _gInitVar
00000004 C _gUnInitVar
0000001c T _main
U _printf
00000004 d _StaticVar.2014
00000000 b _StaticVar2.2015
- 使用ld作为链接器生产可执行文件时,一般会在ld的链接脚本中定义一些特殊符号。只有在使用ld链接生产最终可执行文件的时候,这些符号才会存在。几个代表的特殊符号:
(1)__executable_start,该符号为程序的起始地址,是程序最开始的地址,不是入口地址。
(2)__etext或_etext或etext,该符号为代码段结束地址,即代码段最末尾的地址。
(3)_edata或edata,该符号为数据段结束地址,即数据段最末尾的地址。
(4)_end或end,该符号为程序结束地址。 - 这些特殊符号不需要定义,只要在ld链接脚本中声明,就可以使用,在链接时会解析正确的值。都是虚拟地址。
- 对C/C++来说,编译器默认函数和初始化了的全局变量为强符号;未初始化的全局变量为弱符号。强符合和弱符号只是针对定义,声明没有这个概念。实例如下:
extern int ext; //是一个外部变量的引用声明,因此即不是强符合也不是弱符号。
int weak; //未初始化的全局变量,是弱符号
int strong = 1; //初始化的全局变量,是强符号。
__attribute__((weak)) int weak2 = 2; //初始化的全局变量,但被强制转成弱符号。
int main()
{
return 0;
}
- 链接器会按照一定规制来处理与选择多次定义的全局符号:
(1)规则1:不允许强符号多次定义,否则链接器报重复定义。
(2)规则2:若一个符号在某个目标文件中是强符号,在其他文件是弱符号,那么会选择强符合。
(3)规则3:若一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
5. 调试信息
- 在gcc编译时加上”-g“参数,编译器就会产生目标文件里面的调试信息。如下,出现debug等段名。
命令:gcc -c -g demo.c
demo.o: file format pe-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000005c 00000000 00000000 000001f4 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 00000000 00000000 00000250 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 00000000 00000000 00000000 2**2
ALLOC
3 .drectve 0000001c 00000000 00000000 00000258 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .rdata 00000004 00000000 00000000 00000274 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .debug_info 00000220 00000000 00000000 00000278 2**0
CONTENTS, RELOC, READONLY, DEBUGGING
6 .debug_abbrev 000000e5 00000000 00000000 00000498 2**0
CONTENTS, READONLY, DEBUGGING
7 .debug_aranges 00000020 00000000 00000000 0000057d 2**0
CONTENTS, RELOC, READONLY, DEBUGGING
8 .debug_line 0000006f 00000000 00000000 0000059d 2**0
CONTENTS, RELOC, READONLY, DEBUGGING
9 .debug_str 00000000 00000000 00000000 00000000 2**0
READONLY, DEBUGGING
10 .rdata$zzz 00000024 00000000 00000000 0000060c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
11 .eh_frame 00000058 00000000 00000000 00000630 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
- 在linux下,可以通过命令strip demo.o将目标文件中的debug信息给删除。
第六章 可执行文件的装载与进程
1. 进程虚拟地址空间
- 程序和进程的区别:程序(狭义上是指可执行文件)是一个静态的概念,它是将预先编译好的指令和数据集合在一个文件上;进程是一个动态的概念,它是程序运行时的一个过程。
- 硬件决定了地址空间的最大理论上限,比如:32位的硬件平台决定虚拟地址空间的地址是0-2^32-1,即0x00000000-0xFFFFFFFF,也就是4G的虚拟空间大小。
- 4GB的32位Linux系统,其进程的虚拟地址被划分成操作系统1GB,用户进程3GB。
- 装再方式:覆盖装入和页映射。都是动态装载方法,采用的思想是程序用到哪个模块就将哪个模块装入内存,如果不用就暂时不装入,放在磁盘内。
1.1 覆盖装入
- 覆盖装入是将模块按照它们之间的调用依赖关系组织成树状结构,且禁止跨树间调用。
- 解读以上实例:Overlay Manager是覆盖管理器,是放在内存常驻区。
(1)比如程序执行E模块,那么模块B和模块main必须都在内存中,因为调用模块E后,需要正确返回到模块B和模块main中。
(2)模块C不可以调用模块D/B/E/F,因为不能跨树调用。
从上述可以看出,覆盖装入这种方式是用时间换取空间的方法,目前一些嵌入式开发的内存受限,比如DSP等会采用这种方式。 - 页映射是将内存和所有磁盘中的数据和指令按照“页”为单位划分成若干个页。每页的大小一般为4096Byte、8192Byte、2M、4M等。
- 虚拟内存区域(VMA,Virtual Memory Area):是进程虚拟空间的一个段名。
- 一个进程基本上可以分为以下几种VMA区域:
(1)代码VMA,权限只读、可执行,有映像文件(即可执行文件)。
(2)数据VMA,可读写,可执行,有映像文件。
(3)堆VMA,可读写,可执行,无映像文件,可向上扩展。
(4)栈VMA,可读写,不可执行,无映像文件,可向下扩展。
- 常用的malloc()内存分配函数就是从堆里面分配。
第七章 动态链接
1. 为什么要动态链接
- 静态链接的优缺点:
(1)优点:能够相对独立开发程序的各个模块。
(2)缺点:浪费内存和磁盘空间,模块因耦合度高而更新困难。 - 动态链接库的基本思想:是先不对组成程序的目标文件进行链接,而是等到程序运行时才进行链接。
- 动态链接库还可以在程序运行时动态地选择加载到各种程序模块,可以用来制作程序的插件,易扩展程序功能。
- 动态链接库的扩展名:.so(Linux)、.dll(Windows)。
- 动态链接库比静态链接库的性能要低5%以下。相当于是牺牲性能换取程序在空间上的节省和构建升级时的灵活性。
- 静态链接库的重定位是叫做链接时重定位;动态链接库的重定位是装载时重定位,也叫基址重置。
第十章 内存
1. 程序的内存布局
- 程序的环境主要由:内存、运行库、系统调用或API接口组成。
- 内存是承载程序运行的介质。
- 应用程序无法直接访问的一段内存,被用来给内核使用,称为内核空间。比如:32位的寻址能力,对应4GB的内存,对Windows一般分配2GB给内核使用,也可以分配1GB;Linux一般默认将高地址的1GB给内核使用。剩下的内存时用户空间。
- 用户空间里一般有以下默认区域:
(1)栈:栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。栈通常在用户空间的最高地址出分配,大小一般为数兆字节,向下(向低地址)扩展。
(2)堆:堆是用来容纳应用程序动态分配的内存区域。 程序使用malloc和new来分配的内存就是来自堆内。堆通常是放在栈的下方,大小比栈大得多,一般有几十兆到几百兆字节,向上(高地址)扩展的。
(3)可执行文件映像:存储可执行文件在内存里的映像。
(4)保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称。
2. 栈与调用惯例
2.1 栈
- 实例解读:
(1)实例中的栈底是0xbfffffff,而esp寄存器是栈顶。栈总是向下增长的。压栈(push)使得栈顶的地址减小,弹出(pop)使得栈顶的地址增大。栈是先进后出的。
(2)栈保存了一个函数调用所需要的维护信息。通常包括:函数的返回地址和参数;临时变量:函数的非静态局部变量和编译器自动产生的其他临时变量;保存上下文:包括在函数调用前后需要保持不变的寄存器。
2.2 调用惯例
- 函数的调用方和被调用方对于函数如何用需有一个明确的约定,只有双方都约定时,函数才能被正确调用,这种约定就是调用惯例。
- 一个调用惯例一般规定如下内容:
(1)函数参数的传递顺序和方式。
(2)栈的维护。
(3)函数名字修饰的策略。
- 分析实例
#include <stdio.h>
int Add(int n, int m)
{
int sum;
sum = n + m;
return sum;
}
void main()
{
int Result;
Result = Add(2,3);
printf("Result = %d\n",Result);
}
该实例时按照cdecl的调用惯例,具体堆栈操作如下:
(1)从右至左顺序压栈:所以先执行将m压入栈,再将n压入栈中。函数名字被修饰成_Add。
(2)调用_Add函数,先将返回地址压入栈中,然后跳到_Add执行。
(3)进入_Add函数后,栈上大致时如下图所示:
3.
4. 几项常见的调用惯例
3. 堆与内存管理
- 因为栈上的数据在函数返回的时候会被释放掉,因此无法将数据传递到函数外部,但全局变量没法动态产生,只能编译的时候定义,因此需要借助堆的内存。
- 堆是一块巨大的内存空间,在该空间里程序可以请求一块连续内存,这块内存在程序主动放弃之前都会一直保持有效。
- 申请堆空间的实例:
#include <stdio.h>
#include <malloc.h>
void main()
{
char *p = (char *)malloc(1000*sizeof(char));
free(p);
}
第十一章 运行库
1. 入口函数和程序初始化
- 一个典型的程序运行步骤大致如下: