本文原载于我的博客,地址:https://blog.guoziyang.top/archives/20/
计算机的最小的可寻址的内存单位是字节(byte),8位的块。
C语言的指针的值是某个存储块的第一个字节的虚拟地址。
16进制转2进制:一位16进制数转换为4位2进制数,相反同理
计算机的字长,是指针数据的位长,表明了虚拟地址空间的大小
小端法:最低有效字节在最前面(倒序)。
位向量可用于表示有限集合,如位向量a=[01101001]表示集合A={0, 3, 5, 6}。此时,布尔运算|和&分别对应集合的并和交,~对应于集合的补。
位级运算可用于掩码运算(从一个字中选出指定位),如x&0xFF(最低8位为1)选出最低有效字节。
逻辑右移在左端补k个0,算数右移在左端补k个最高有效位的值(符号位),以保证符号不变。左移都是补0。几乎所有的编译器都采用算数右移。
对于二进制转无符号数:
B 2 U ω ( x ⃗ ) = ∑ i = 0 ω − 1 x i 2 i B2U_{\omega}(\vec{x})=\sum_{i=0}^{\omega-1}x_i2^i B2Uω(x)=i=0∑ω−1xi2i
在计算机中有符号数的表示是补码,补码的最高有效位是负权(符号位),二进制数转化为有符号数(补码):
B 2 T ω ( x ⃗ ) = − x ω − 1 2 ω − 1 + ∑ i = 0 ω − 2 x i 2 i B2T_{\omega}(\vec{x})=-x_{\omega-1}2^{\omega-1}+\sum_{i=0}^{\omega-2}x_i2^i B2Tω(x)=−xω−12ω−1+i=0∑ω−2xi2i
强制类型转换保证位向量不变(二进制数不变),只是改变解释这些位的方式。于是有符号数转无符号数就定义为:将有符号数转为二进制数,再将这个二进制数转为无符号数。
根据以上原理,补码(有符号数)转换为无符号数:
KaTeX parse error: No such environment: equation at position 8: \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲T2U_\omega(x)=\…
同理,无符号数转为补码:
KaTeX parse error: No such environment: equation at position 8: \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲U2T_\omega(u)=\…
如果参与一个运算的运算数一个有符号另一个无符号,C语言当作无符号算数处理。
扩展一个无符号数的位长度往往使用零扩展,而有符号数(补码)则采用符号扩展(补充符号位)。
强制类型转换:先执行位长度转换(扩展,如果有),再执行有无符号转换。
当截断一个无符号数时,仅仅是丢弃其多出的位。有符号数截断同样直接丢弃,只是此时留下的最高位称为其符号位。
B 2 U k [ x k − 1 , x k − 2 , . . . , x 0 ] = B 2 U ω ( [ x ω − 1 , x ω − 2 , . . . , x 0 ] m o d 2 k ) B2U_k[x_{k-1},x_{k-2},...,x_0]=B2U_\omega([x_{\omega-1},x_{\omega-2},...,x_0]mod2^k) B2Uk[xk−1,xk−2,...,x0]=B2Uω([xω−1,xω−2,...,x0]mod2k)
B 2 T k [ x k − 1 , x k − 2 , . . . , x 0 ] = U 2 T k ( B 2 U ω ( [ x ω − 1 , x ω − 2 , . . . , x 0 ] ) m o d 2 k ) B2T_k[x_{k-1},x_{k-2},...,x_0]=U2T_k(B2U_\omega([x_{\omega-1},x_{\omega-2},...,x_0])mod2^k) B2Tk[xk−1,xk−2,...,x0]=U2Tk(B2Uω([xω−1,xω−2,...,x0])mod2k)
无符号数加法如果溢出,将会直接截断溢出的位,即在正常的结果上减去 2 ω 2^\omega 2ω。
有符号数加法如果正溢出,则直接截断溢出位,即在正常的结果上减去 2 ω 2^\omega 2ω;如果负溢出,则加上 2 ω 2^\omega 2ω。由此可推断,当两个数操作数都大于0而结果小于等于0时,说明发生了正溢出;当两个操作数都小于0,而结果大于等于0时发生了负溢出。两个都是充要条件。
补码的非:除了TMin外,所有补码的非的数值都是自己的相反数,TMin的非是它自己。
无符号数乘法直接溢出截断,有符号数同样截断。即,两者乘法的位级表示相同。
x ∗ y = ( x ⋅ y ) m o d 2 ω x ∗ y = U 2 T ω ( ( x ⋅ y ) m o d 2 ω ) x*y=(x·y)mod2^\omega\\ x*y=U2T_\omega((x·y)mod2^\omega) x∗y=(x⋅y)mod2ωx∗y=U2Tω((x⋅y)mod2ω)
左移一个数值等价于执行一个与2点幂相乘的无符号乘法。除法通常使用逻辑右移来优化。
IEEE浮点标准用 V = ( − 1 ) s × M × 2 E V=(-1)^s\times M\times 2^E V=(−1)s×M×2E,其中s为符号位,M是尾数,E是阶码(加权)。将浮点数的位表示划分为3个字段:一个单独的符号位s编码符号s;k位阶码字段exp编码为阶码E( E = e x p − b i a s E=exp-bias E=exp−bias(规格化)或 E = 1 − b i a s E=1-bias E=1−bias(非规格化),其中 b i a s = 2 k − 1 − 1 bias=2^{k-1}-1 bias=2k−1−1,k为阶码字段位数);n位小数字段frac编码尾数M。
根据exp的值,可将单精度浮点数分为四类:1. 规格化数,exp不全为0也不全为1;2. 非规格化数,exp全为0;3. 无穷大,exp全为1且小数frac全为0;4. NaN,exp全为1且小数frac不全为0。
IEEE754标准规定的默认舍入原则是最近舍入原则,即在小数部分小于0.5时使用向下舍入,大于0.5时向上舍入,而等于0.5时向偶数舍入。
当低精度向高精度强制类型转换时不会溢出,但可能发生舍入。范围小可能造成溢出,精确度小可能造成舍入。
处理器无法从低级语言推断出高级语言。
Intel用术语“字”来表示16位数据(2个字节)。
根据传送数据的不同,mov指令分为四种:movb(传送字节)、movw(传送字)、movl(传送双字)、movq(传送四字)。生成一字节和两个字节的指令会保证其它字节不变,生成四个字节的指令会将高位四字节置为0。movabsq传送绝对四字,用于传送64位立即数,而普通的movq只能支持转送32位立即数。
内存引用寻址模式: I m m ( r b , r i , s ) Imm(r_b,r_i,s) Imm(rb,ri,s),其中出现的四个符号分别为立即数偏移、基址寄存器、变址寄存器和比例因子,计算为 I m m + R [ r b ] + R [ r i ] ⋅ s Imm+R[r_b]+R[r_i]\cdot s Imm+R[rb]+R[ri]⋅s。其它都是该寻址方式的特殊情况。
传送指令mov的两个操作数不能同时为内存地址,将数据从一个内存位置复制到另一个内存位置需要两步,由寄存器中转。
movz传送零扩展的数据,如movzwq指将做了零扩展的字传送到四字。同理movs传送符号扩展到数据。
lea(加载有效指令)可用于实现一些算术运算。如leaq (%rdi, %rsi, 4), %rax表示将%rax的值赋为%rdi+%rsi*4。
subq %rax, %rdx表示%rdx = %rdx - %rax。
CPU有一组条件码寄存器,每个寄存器一个位,描述最近的算数或逻辑操作的属性。CF:进位标志;ZF:零标志;SF:符号标志(负);OF:溢出标志。
有两个指令只改变条件码而不改变寄存器。CMP行为与SUB一样,使用后一个寄存器的值减去前一个寄存器,用于比较大小。TEST指令和AND指令行为一样,testq %rax, %rax用于检查%rax是否为0。
setx和jx指令根据条件码来进行操作。如sete(setne)在相等(不相等)时设置。sets(setns)在是负数(不是负数)时设置。同理g、ge表示有符号的大于和大于等于,l和le表示有符号的小于和小于等于。a和ae表示无符号的大于和大于等于,b和be表示无符号的小于和小于等于。
分支预测错误的处罚的计算:设预测错误的概率为p,可预测(无错误)的代码执行时间为 T O K T_{OK} TOK,分支行为模式随机时代码执行时间 T r a n T_{ran} Tran,预测错误的处罚时间 T M P T_{MP} TMP,则有
T r a n = T O K + p T M P T_{ran}=T_{OK}+pT_{MP} Tran=TOK+pTMP
imulq:有符号全乘法;mulq:无符号全乘法;clto:转换为八字;idivq:有符号数除法;divq:无符号数除法。
当执行call指令时,将当前指令(call)的下一条指令push进栈,并将%rip寄存器的值设置为被调用函数的入口地址。当被调用函数执行ret指令时,将栈顶元素(刚刚push进的地址)弹出并设置给%rip。
当调用函数时,参数默认存储的寄存器位置(顺序):%rdi、%rsi、%rdx、%rcx、%r8、%r9。超出六个参数部分要使用栈来传递。返回值通常存储在%rax中。
对抗缓冲区溢出攻击的方法:栈随机化、栈破坏检测(检测金丝雀值)、限制可执行代码区域(页权限管理)。
%rbp:帧指针,指向栈帧的底部,用于解决变长栈帧。
CISC(复杂指令集计算机)代表:x86架构,RISC(精简指令集计算机)代表:ARM架构。
CPU处理指令分为6个阶段:
如果一个表达式总是得到相同的结果,则将其移动到循环外面(消除循环的低效率、减少过程调用)。
减少不必要的内存引用,表现是用局部变量暂时保存循环中间量(如累加),直到最后在写入内存位置。汇编中是暂时保存在寄存器中。
超标量处理器,一个时钟周期可以执行多条操作,且乱序执行。该设计分为两个部分:指令控制单元(ICU)和执行单元(EU)。ICU从内存中读出指令序列,并生成一组基本操作。
处理器使用分支预测技术猜测是否会选择分支,以及分支的目标地址。同时使用投机执行技术,开始取出分支会跳到的地方的指令,并译码(即使是在预测之前)。如果预测错误,,将会将状态重新设置到分支点到状态,并取出和执行另一个方向的指令。
在ICU中,退役单元记录正在进行的处理,并确保其遵守机器级程序的顺序语义。一旦一条指令的操作完成了,且所有引起这条指令的分支被认为预测正确,那么这条指令就可以退役了,对所有程序寄存器的更新都可以被实际执行了。而如果某个分支点预测错误,这条指令会被清空,丢弃所有计算结果。这样,错误的分支预测就不会改变程序的状态。
磁盘对数据的访问时间有三个主要部分组成:寻道时间、旋转时间、传送时间。通常,一个磁盘的寻道时间是固定的。平均旋转时间是磁盘转动一圈的一半。平均传送时间(一个扇区)是读写头转完一个扇区的时间。
局部性分为时间局部性和空间局部性。时间局部性指引用之前引用过的内存位置的附近位置。空间局部性指引用刚刚引用过的内存位置。利用局部性可以提高程序的性能。重复引用相同变量可以提高时间局部性,步长为k的引用模式,步长越小空间局部性越好。循环体越小,局部性越好。
存储器层次:寄存器,高速缓存,主存,磁盘,远程存储……越往上,存储器越小、越快、成本更高。
缓存不命中的分类:
LRU替换策略,驱逐组中最近最少被使用的块。
直写和写回:
描述高速缓存的参数: S = 2 s S=2^s S=2s,缓存组数;E:每个组的行数; B = 2 b B=2^b B=2b:每个缓存块的字节数(一行仅有一个缓存块!);m:地址位数。缓存地址的构成:t位标记位,s位组索引,b位块偏移。缓存行的构成:有效位,标记位,高速缓存块。
直接映射高速缓存:每个组只有一个行(E=1)。抖动指高速缓存反复地加载和驱逐相同的高速缓存块的组。
组相连高速缓存,一个组中有多个行,用于消除直接映射高速缓存冲突不命中的问题。E路组相连高速缓存指每组有E行。
全相联高速缓存,只有一个组(S=1)。全相联高速缓存不会发生冲突不命中。TLB是全相联的。
貌似,三级缓存中每个块大小都是64字节。
编写缓存友好的代码:
利用分块来提高时间局部性和缓存友好程度:分块是将程序中的数据组织成块(不是高速缓存块),这样构造可以使一次加载一个块到缓存中,对这个块进行所有读写操作,然后丢掉这个块,加载下一个块。
链接可以执行于编译时(静态),也可以执行于加载时(动态),甚至运行时(动态)。
编译器驱动程序,代表用户在需要时调用预处理器、编译器、汇编器和链接器。一个C语言程序生成可执行文件的过程:预处理、编译、汇编、链接。
静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件。任务:符号解析,将每个符号的引用和一个符号的定义关联起来;重定位,把每个符号定义与一个内存位置关联起来,从而重定位这个节。
可重定位目标文件由各种不同的代码和数据节(section)组成。
Linux的目标文件组织格式是ELF格式。
ELF可重定位目标文件的格式:ELF头(系统信息,ELF头大小、目标文件类型、节头部表偏移、节头部表条目大小和数量)、节(数据、代码)、节头部表(描述节的位置和大小,每个节都有一个固定大小的条目)。
典型的ELF可重定位目标文件包含以下的节:
每个可重定位目标文件中都包含一个符号表(.symtab),包含模块定义和引用的符号信息。在链接器上下文中,有三种不同的符号:
在.symtab的符号表中有三个伪节,在节头部表中没有条目。ABS,不该被重定位的条目;UNDEF,未定义的符号(本模块引用,其它模块定义);COMMON,为被分配位置的未初始化的数据目标。COMMON和.bss的区别很细微,GCC将未初始化的静态变量以及初始化为0的全局或静态变量符号分配到.bss节,而将未初始化的全局变量分配到COMMON节。
链接器解析符号引用的方式是将每个引用和它输入的可重定位目标文件的符号表中的一个确定的符号关联起来。
对于全局符号的引用,当编译器遇到一个不是在当前模块定义的符号时,会假设该符号是在其它某个模块中定义的,生成一个链接器符号表条目,并交给链接器处理。
强符号弱符号(仅针对全局变量和函数):函数和已初始化的变量是强符号,未初始化的变量是弱符号。链接器使用以下规则来处理多重定义的符号:
当编译器在编译某个模块时,遇到一个弱全局符号,由于无法确定其它模块是否定义该同名符号,所以编译器仅仅是将其分配给COMMON。如被初始化为0,则是一个强符号,分配给.bss。同样,静态符号的构造就必须唯一,所以编译器将其分配给.data或.bss。
静态链接库,一个.a文件。相关函数被编译为独立的目标文件,并封装为单独的静态库文件。于是在链接时,链接器将只复制被程序引用的目标模块。
链接器通过静态库来解析引用的流程。链接器维护一个可重定位目标文件的集合E,一个未解析的符号(已引用未定义)集合U,以及一个在前面输入文件已经定义的符号集合D。初始都为空。对于每个输入文件f,链接器会判断f是目标文件还是存档文件(静态库)。
重定位,合并输入模块,并为每个符号分配运行时地址。分为两步:
代码的重定位条目放在.rel.text中,已初始化的数据的重定位条目放在.rel.data中。重定位类型中有两种主要的:R_X86_64_PC32,用一个32位值加上PC的当前运行值;R_X86_64_32,直接使用一个32位绝对地址。例子在P481。
重定位PC相对地址求法,链接器事先确定引用的节基地址(ADDR(s))和被引用符号的定义地址(ADDR(r.symbol)),要求引用相对于PC的偏移(refptr)。可以先计算出引用的运行时地址redaddr, r e f a d d r = A D D R ( s ) + r . o f f s e t refaddr = ADDR(s) + r.offset refaddr=ADDR(s)+r.offset,接着计算执行到引用时PC的值为 A D D R ( r . s y m b o l ) + r . a d d e n d ADDR(r.symbol)+r.addend ADDR(r.symbol)+r.addend。最有用PC的值减去符号定义地址(refaddr)即可。
重定位绝对引用地址求法,已知符号的定义地址(ADDR(r.symbol)),求绝对地址(refptr),只需要用ADDR(r.symbol)加上r.addend即可。
ELF可执行文件的结构:ELF可执行文件依旧由多个节构成,先是ELF头,接着是段头部表(指导映射到内存),接着是数据代码等节,最后是节头部表(描述各个节的信息),由于被完全链接,不包含.rel重定位节。其中,ELF头、段头部表、.init、.text、.rodata被映射到只读内存段(代码段),.data、.bss被映射到读写内存段(数据段)。
运行时内存映像结构:代码段从虚拟内存0x400000处开始,后面是数据段,接着是运行时堆,通过malloc向上增长,堆后面有一块为共享模块保留的区域。用户栈是从最大合法用户地址( 2 48 − 1 2^{48}-1 248−1)开始向小地址增长,栈上的区域从 2 48 2^{48} 248开始是为内核中的代码和数据保留的。
当加载器运行时,创建内存映像,在程序的段头部表的指导下,加载器将文件的片复制到代码段和数据段(实际上不会真正地复制,仅仅是映射),接着加载器跳转到程序的入口(_start)函数地址。
动态链接共享库在Linux中后缀为.so。所有引用该库的可执行目标文件共享这个.so的代码和数据,在内存中,一个共享库的.text节的一个副本可以被多个正在运行的进程共享。一个被动态链接的可执行目标文件中有.interp节,该节包含链接器的路径名。
当加载器加载和运行动态链接的可执行目标文件时,从.interp中找到动态链接器的路径,并加载和运行动态链接器,动态链接器将重定位(映射,复制)共享库的文本和数据到内存段中,并重定位可执行目标文件的所有由共享库定义的符号的引用,接着动态链接器将控制传递给应用程序,此时,共享库的内存位置固定,程序开始运行。
位置无关代码(PIC),保证共享库代码可以是不确定的,且即使共享库代码的长度发生变化,也不会影响调用它的程序。PIC是为了链接器无须修改代码即可将共享库加载到任意位置运行。
以上是加载时动态链接,运行时动态链接在代码中显式调用dlopen函数加载动态链接库,并使用dlsym函数获取库中某个特定的函数。
异常是指控制流中的突变(非相邻地址跳转),并不是错误。
任何情况下,当处理器检测到有事件发生,就会通过一张叫做异常表的跳转表(该起始地址放在一个异常表基址寄存器中),进行一个间接过程调用(异常),到一个专门用来处理这类事件的操作系统子程序(异常处理程序),完成处理后,根据异常类型,将发生以下情况之一:
处理程序将控制返回给当前指令,即事件发生时正在执行的指令。(重新执行)
处理程序将控制返回给当前程序的下一条指令,即如果没有发生异常将会执行的下一条指令。(继续执行)
处理程序终止被中断的程序。(终止执行)
注意,异常处理程序运行在内核模式,需要从用户模式转换到内核模式。
异常分为四类:中断、陷阱、故障和终止。
异常举例:
系统调用通过syscall指令提供,到Linux的系统调用的参数由寄存器传递,%rax中包含了系统调用号。重要的系统调用号:0,read,读文件;1,write,写文件;2,open,打开文件;3,close,关闭文件。
进程是一个执行中程序的实例。程序都运行在进程的上下文中,上下文包括程序代码和数据、栈、寄存器内容、PC、环境变量和打开的文件描述符。进程给应用程序的抽象:一个独立的逻辑控制流(独占处理器),一个私有的地址空间(独占内存)。
一个逻辑流的执行在时间上与另一个流重叠,称为并发流。如果两个流并发地运行在不同的处理器核或计算机上,则称它们为并发流。
进程从用户模式进入内核模式的唯一方法是通过诸如中断、故障或陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将进入内核模式,处理程序运行在内核模式下,当它返回到应用程序代码时,处理器变为用户模式。
内核可以决定抢占当前进程,并重新开始一个之前被抢占的进程,这种决策称为调度。上下文切换:1.保存当前进程的上下文;2.恢复某个先前被抢占的进程被保存的上下文;3.将控制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能发生上下文切换。如果系统调用因为等待某个事件而发生阻塞,那么内核执行调度切换到另一个进程。
进程处于一下三种状态之一:
fork函数调用一次,返回两次,在父进程中返回子进程的PID,在子进程中返回0。父子进程是并发执行的。
当一个进程终止时,进程被保持在一个已终止的状态,直到被它的父进程回收。一个终止但未被回收的进程称为僵尸进程。如果父进程已终止,内核安排init进程(PID=1)去回收。
execve函数在当前进程的上下文中加载并运行一个新的程序。调用一次,从不返回。
一条信号就是一条小消息,它通知进程系统中发生了一个事件。内核通过更新进程上下文中的某个状态,发送一个信号给目的进程。
进程可以忽略信号、终止或者通过执行一个信号处理程序的用户层函数捕获这个信号。任何时刻,一种类型都至多有一个待处理信号,如果某时刻进程有一个某种类型的待处理信号,那么任何接下来发送到该进程的同类型信号都会被丢弃。
Unix系统的信号发送机制是基于进程组的,默认,一个子进程和它的父进程同属于一个进程组。
ctrl+c会导致内核发送一个SIGINT信号给前台进程组的每个进程,结果是终止前台作业;ctrl+z会发送SIGTSTP信号到前台进程组的每个进程,默认停止(挂起)前台作业。
当内核将进程从内核模式切换到用户模式时(系统调用返回或上下文切换),会检查进程未被阻塞的待处理信号集合,如果集合非空,内核选择集合中的某个信号(通常是最小的),并强制进程接收。
每个信号又有预定义的默认行为:终止,终止并转储,停止(挂起)直到被SIGCONT信号重启,忽略信号。
Linux提供的阻塞信号的隐式和显式的机制:
Linux的信号阻塞机制的问题:如果存在一个未处理的信号就表明至少有一个信号到达了。
非本地跳转,将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过调用-返回序列。非本地跳转是通过setjmp和longjmp实现的。
CPU通过内存管理单元(MMU)将虚拟地址转换为物理地址,MMU通过存放在内存中的查询表来动态翻译虚拟内存。
通常,物理页和虚拟页大小相等。虚拟页面有三种状态:未分配的、缓存的、未缓存的。
术语DRAM缓存表示虚拟内存系统的缓存,它在主存中缓存虚拟页。由于大的不命中处罚,虚拟页通常很大(4KB),且是全相联的,采用写回策略。
页表通常存在于物理内存中,页表是一个页表条目(PTE)的数组。虚拟地址空间的每个页在页表的一个固定偏移量处都有一个PTE。
通常,一个PTE由一个有效位和一个n位地址字段组成,有效位表明该虚拟页是否被缓存在DRAM中。如果有效位有效,则地址字段就是DRAM中相应物理页的其起始位置。如果没有设置有效位,若地址字段为空,则表明该虚拟页非被分配,否则该地址指向虚拟页在磁盘上的起始位置。
DRAM缓存不命中称为缺页,缺页会触发一个缺页异常,缺页异常调用内核的缺页异常处理程序,该程序驱逐一个牺牲页,如果牺牲页已经被修改就会被先复制会磁盘,接着,内核从磁盘复制虚拟页到物理页,并更新PTE,接着处理程序返回,重新执行造成缺页异常的指令。
虚拟内存的作用:缓存、内存管理、内存保护。
操作系统为每个进程都提供了一个独立的页表,于是每个进程都有一个独立的虚拟地址空间。虚拟地址简化了链接和加载、代码和数据共享,以及内存分配。
虚拟内存作为内存保护的工具,每个PTE上可以带一个许可位来表明虚拟页面的权限,当访问时违反了权限,CPU触发一般保护故障,掉用异常处理程序,该异常称为段错误。这个机制可用于防止缓冲区溢出。
地址翻译时的符号: N = 2 n N=2^n N=2n,虚拟地址空间的地址数量; M = 2 m M=2^m M=2m,物理地址空间的地址数量;P,页的大小(虚拟页和物理页一致);VA(虚拟地址)的组成:VPO,虚拟页面偏移量;VPN,虚拟页号;TLBI,TLB索引;TLBT,TLB标记;PA(物理地址)的组成:PPO,物理页面偏移量(通常和VPO相同);PPN,物理页号;CO,缓冲块内的字节偏移量;CI,高速缓存索引;CT,高速缓存标记。
当使用TLB加速地址翻译时,VPN被分为TLB标记和TLB索引。当使用k级页表时,VPN被等分为k份,每份是一级页表的偏移量。多级页表下,只有一级页表常驻内存。
Intel Core i7,x86-64:四级页表,三级缓存,缓存块大小64字节。支持64位虚拟内存地址,实际使用48位。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,堆向地址高处生长,内核维护者一个变量brk指向堆顶。malloc包属于一个显式分配器,要求显式地释放(free)内存块。
隐式空闲链表,空闲块通过块头部的大小字段隐含地连接在一起。遍历空闲块就需要便利所有的块。块添加一个脚部用于合并前一块。
显式空闲链表,在空闲块中包含前驱和后继指针,指向前一个或后一个空闲块。这种情况需要耗费较多的空间。可能造成空闲块增多。维护显式空闲链表有两种方式:后进先出(释放的块放在开头)和地址顺序。
隐式分配器需要垃圾收集,垃圾收集器将内存视为一张有向图。根节点可以是寄存器、栈里的变量或者虚拟内存读写数据区的全局变量。