Linux内存管理之内存寻址:分段机制的实现方式

Table of Contents

linux中的分段机制

linux中的GDT

用户态和内核态的数据段以及代码段4个段

任务状态段TSS

寄存器保存区域

内层堆栈指针区域

地址映射寄存器区域

链接字段

其它字段

3个局部线程存储(Thread-Local Storage,TLS)段

与高级电源管理(AMP)相关的3个段

与支持即插即用(PnP)功能的BIOS服务程序相关的5个段

处理”双重错误”异常的特殊TSS段

linux中的LDT

段选择符

段描述符

段描述符表

GDTR全局描述符寄存器

LDTR局部描述符寄存器

linux中GDT,LDT和IDT结构定义

GDT描述符表gdt_page定义

段描述符结构定义

ia32机器上的定义

x64机器上的定义

系统GDT,IDT指针描述结构

参考文章


 

linux中的分段机制


前面说了那么多关于分段机制的实现,其实,Linux以非常有限的方式使用分段。因为,Linux基本不使用分段的机制(注:并不是不使用,使用分段方式还是必须的,会简化程序的编写和运行方式),或者说,Linux中的分段机制只是为了兼容IA32的硬件而设计的。实际上,分段和分页在某种程度上显得有些多余,因为它们都可以划分进程的物理地址空间,分段可以给每一个进程分配不同的线性地址,而分页可以把同一线性地址,映射到不同的物理地址空间

与分段相比,linux更喜欢分页方式,因为:

  • 当所有进程使用相同的段寄存器值时,内存管理变得简单,因为他们可以共享同样的一组线性地址,或者更通俗的说,虚拟地址与线性地址一致。
  • linux设计目标之一是可以把它移植到绝大多数流行的处理器平台。然而,RISC体系结构的对分段支持很有限。

Intel微处理器的段机制是从8086开始提出的, 那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。

从2.2版开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,因为就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。2.6版的linux也只有在80x86结构下才使用分段。

在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作 系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。 但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移植性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。 万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说 “偏移量=线性地址”。

另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说 虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。

 

linux中的GDT


在单处理器的系统中只有一个GDT,但是在多处理器系统中每个CPU对应一个GDT。

所有的GDT均存储在cpu_gdt_table数组中,而所有GDT的地址和它们的大小被存放在cpu_gdt_descr数组中。

每个GDT包含18个段和14个空的、未使用的或者保留的项。插入未使用的的目的是为了使经常一起访问的描述符能够在处于同一个32字的硬件告诉缓冲行中。

而那些被使用的18个段必定是如下几种段类型

①全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。

②段选择器(Selector)由GDTR访问全局描述符表是通过“段选择”(实模式下的段寄存器)来完成的。为了访问一个段,一个Pentium程序必须把这个段的选择子装入机器的6个段寄存器的某一个中。在运行过程中,CS寄存器保存代码段的选择子,DS寄存器保存数据段的选择子。每个选择子是一个16位数。

选择子中的一位指出这个段是局部的还是全局的(它是在LDT中还是在GDT中),其他的13位索引是LDT或GDT的表项编号,表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符,然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址。因此,这些表的长度被限制在最多容纳8K个段描述符。

段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。

例如给出逻辑地址:21h:12345678h转换为线性地址

  • a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1
  • b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

 

用户态和内核态的数据段以及代码段4个段


由于IA32段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB 的段描述符。

不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据IA32段保护机制规定,特权级3的程序是无法访问特权级为 0的段的,所以Linux必须为内核用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。

相应的段描述符由宏__USER_CS, __USER_DS, __KERNEL_CS和__KERNEL_DS分别定义。因此为了对内核代码段寻址,内核只需要将__KERNEL_CS的值装载进CS段寄存器即可。

注意
与段相关的线性地址从0开始,达到的寻址长度。这就意味着在用户态和内核态下所有进行均可使用相同的逻辑地址。

而所有的段都是从地址0x00000000开始的,我们可以知道在linux下逻辑地址与线性地址一致(linux并没有过多的使用分段技术),即逻辑地址的偏移量字段与相应的线性地址字段的值是一致的。

 

任务状态段TSS


  TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。

  TSS在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。

在任务切换过程中,

  1. 首先,处理器中各寄存器的当前值被自动保存到TR所指定的TSS中;
  2. 然后,下一任务的TSS的选择子被装入TR;
  3. 最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。

由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。
 
  TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的,但在此之外系统软件还可定义若干附加信息。基本的104字节可分为链接字段区域、内层堆栈指针区域、地址映射寄存器区域、寄存器保存区域和其它字段等五个区域。

 

寄存器保存区域


寄存器保存区域位于TSS内偏移20H至5FH处,用于保存通用寄存器、段寄存器、指令指针和标志寄存器。当TSS对应的任务正在执行时,保存区域是未定义的;在当前任务被切换出时,这些寄存器的当前值就保存在该区域。当下次切换回原任务时,再从保存区域恢复出这些寄存器的值,从而,使处理器恢复成该任务换出前的状态,最终使任务能够恢复执行。

各通用寄存器对应一个32位的双字,指令指针和标志寄存器各对应一个32位的双字;各段寄存器也对应一个32位的双字,段寄存器中的选择子只有16位,安排再双字的低16位,高16位未用,一般应填为0。

 

内层堆栈指针区域


  为了有效地实现保护,同一个任务在不同的特权级下使用不同的堆栈。例如,当从外层特权级3变换到内层特权级0时,任务使用的堆栈也同时从3级变换到0级堆栈;当从内层特权级0变换到外层特权级3时,任务使用的堆栈也同时从0级堆栈变换到3级堆栈。所以,一个任务可能具有四个堆栈,对应四个特权级。四个堆栈需要四个堆栈指针。

  但是,当特权级由内层向外层变换时,并不把内层堆栈的指针保存到TSS的内层堆栈指针区域。实际上,处理器从不向该区域进行写入,除非程序设计者认为改变该区域的值。这表明向内层转移时,总是把内层堆栈认为是一个空栈。因此,不允许发生同级内层转移的递归,一旦发生向某级内层的转移,那么返回到外层的正常途径是相匹配的向外层返回。

 

地址映射寄存器区域


  从虚拟地址空间到线性地址空间的映射由GDT和LDT确定,与特定任务相关的部分由LDT确定,而LDT又由LDTR确定。如果采用分页机制,那么由线性地址空间到物理地址空间的映射由包含页目录表起始物理地址的控制寄存器CR3确定。所以,与特定任务相关的虚拟地址空间到物理地址空间的映射由LDTR和CR3确定。显然,随着任务的切换,地址映射关系也要切换。

  但是,在任务切换时,处理器并不把换出任务但是的寄存器CR3和LDTR的内容保存到TSS中的地址映射寄存器区域。事实上,处理器也从来不向该区域自动写入。因此,如果程序改变了LDTR或CR3,那么必须把新值人为地保存到TSS中的地址映射寄存器区域相应字段中。可以通过别名技术实现此功能。

 

链接字段


链接字段安排在TSS内偏移0开始的双字中,其高16位未用。在起链接作用时,地16位保存前一任务的TSS描述符的选择子。

如果当前的任务由段间调用指令CALL或中断/异常而激活,那么链接字段保存被挂起任务的 TSS的选择子,并且标志寄存器EFLAGS中的NT位被置1,使链接字段有效。在返回时,由于NT标志位为1,返回指令RET或中断返回指令IRET将使得控制沿链接字段所指恢复到链上的前一个任务。

 

其它字段


  为了实现输入/输出保护,要使用I/O许可位图。任务使用的I/O许可位图也存放在TSS中,作为TSS的扩展部分。在TSS内偏移66H处的字用于存放I/O许可位图在TSS内的偏移(从TSS开头开始计算)。关于I/O许可位图的作用,以后的文章中将会详细介绍。
 
  在80386中,只定义了一种属性,即调试陷阱。该属性是字的最低位,用T表示。该字的其它位置被保留,必须被置为0。在发生任务切换时,如果进入任务的T位为1,那么在任务切换完成之后,新任务的第一条指令执行之前产生调试陷阱。

 

3个局部线程存储(Thread-Local Storage,TLS)


线程局部存储区(Thread Local Storage, TLS):将数据与一个正在执行的特定函数关联起来。这种机制允许多线程应用程序使用最多3个局部于线程的数据段。

linux系统可以使用set_thread_area()和get_thread_area()分别为正在执行的进程创建和撤销一个TLS段。

线程局部存储是将现有函数变为线程安全的有用技巧。

当一个函数中访问并修改全局或静态变量,那么这个函数就是不可重入的。若使之变为可重入的函数,可以使用线程同步,也可以使用线程局部存储。线程局部存储为每一个访问此变量的线程提供一个此变量独立的副本,线程可以修改此变量,而不会影响到其他线程。

注:通过以上描述可以看出,线程局部存储不是用来共享变量的。
具体可参照 每天进步一点点——Linux中的线程局部存储

与高级电源管理(AMP)相关的3个段


由于BIOS代码使用了分段机制,所以当linux APM驱动程序调用BIOS函数来获取或者设置APM设备的状态时,就可以使用自定义的代码段和数据段。

 

与支持即插即用(PnP)功能的BIOS服务程序相关的5个段


前面一种情况下,就像前述与APM相关的3个段的情况一样,由于BIOS例程使用段,所以当linux的PnP设备驱动程序调用BIOS函数来检测PnP设备使用的资源时,就可以使用自定义的代码段和数据段。

 

处理”双重错误”异常的特殊TSS段


处理一个异常的时候可能会引发另外一个异常,在这种情况下产生双重错误。

 

linux中的LDT


大多数用户态下的linux程序不使用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享。缺省的局部描述符表存放在default_ldt数组中。

如果在某些情况下,进程仍然需要创建自己的局部描述符表,(例如wine这样的程序,他执行面向段的微软windows应用程序),可以使用modify_ldt()系统调用允许进程创建自己的局部描述符表。

modify_ldt() 读取或一个进程写入本地描述符表(ldt)。 ldt 是使用i386处理器每个进程的内存管理表。对于该表的详细信息,请参阅英特尔386处理器手册。

任何被modify_ldt()创建的自定义局部描述符表仍然需要他自己的段。当处理器开始执行拥有自定义局部描述符表的进程时,该CPU的GDT副本中的LDT表项相应的就被修改了。

用户态的程序同样也利用modify_ldt()来分配新的段,但内核却从不使用这些段,它也不需要了解相应的段描述符,因为这些段描述符被包含在进程自定义的局部描述符表中。

③局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图

LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如上图,如果装载的是Selector 2则LDTR指向的是表LDT2。

举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。

  • 1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR
  • 2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11 1 00b)。OFFSET=12345678h。逻辑地址为1C:12345678h
  • 3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h
  • 4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector 1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)

由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。


段选择符

32位汇编中16位段寄存器(CS、DS、ES、SS、FS、GS)中不再存放段基址,而 是段描述符在段描述符表中的索引值,D3-D15位是索引值,D0-D1位是优先级(RPL)用于特权检查,D2位是描述符表引用指示位TI,TI=0指 示从全局描述表GDT中读取描述符,TI=1指示从局部描述符中LDT中读取描述符。这些信息总称段选择符(段选择子).

段描述符:

  • P,present位,1表示所描述的段存在(有效),为0表示所描述的段无效,使用该描述符会引起异常 
  • DPL,Descriptor privilege,描述符特权级别,说明所描述段的特权级别 
  • DT,描述符类型位,1说明当前描述符为存储段描述符,0为系统描述符或门描述符. 
  • TYPE: 
    • 位0:A(accessed)位,表明描述符是否已被访问;把选择子装入段寄存器时,该位被标记为1 
    • 位3:E(EXECUTABLE?)位,0说明所描述段为数据段;1为可执行段(代码段)
    • 当为数据段时, 
      •    位1为W位,说明该数据段是否可写(0只读,1可写) 
      •    位2为ED位,说明该段的扩展方向(0向高位扩展,1向低位扩展) 
    • 当为可执行段是, 
      •    位1为R位,说明该执行段是否可读(0只执行,1可读) 
      •    位2为C位,0说明该段不是一致码段(普通代码段),1为一致码段 
  • G为粒度位,0说明LIMIT粒度为字节,1为4K字节. 
  • D位: 
    • 1.在可执行段中,D为1,表示使用32位地址,32/8位操作数;为0表示使用16位地址,16/8位操作数 
    • 2.在由SS寻址的段描述符(堆栈段?)中,D为1表示隐含操作(如PUSH/POP)使用ESP为堆栈指针,为0使用SP(隐含操作:未明确定义段属性类型USE16/USE32?66H,67H?) 
    • 3.在向低扩展的存储段中,D为1,表示段的上限为4G;为0上限为64K

存储段描述符的结构表示:

分段管理可以把虚拟地址转换成线性地址,而分页管理可以进一步将线性地址转换成物理地址。当CR0中的PG位置1时,启动分页管理功能,为0时,这禁止启动分页管理功能,并且把线性地址作物理地址使用。

虚拟地址转为线性地址:

线性地址= 段基指 + 偏移地址

32位线性地址转为物理地址:

32位分为:

  • 页目录索引:占最高10位,指示页目录表中第几个页表描述符
  • 页表索引:占12位到21位,也是10位。指示这页表中第几个页描述符
  • 页描述符:线性地址的低12位为页内偏移量。

 

段描述符


8个 字节64位,每一个段都有一个对应的描述符。根据描述符描述符所描述的对象不同,描述符可分为三类:储存段描述符,系统段描述符,门描述符(控制描述 符)。在描述符中定义了段的基址,限长和访问内型等属性。其中基址给出该段的基础地址,用于形成线性地址;限长说明该段的长度,用于存储空间保护;段属性 说明该段的访问权限、该段当前在内存中的存在性,以及该段所在的特权级。

 

段描述符表


IA-32处理器把所有段描述符按顺序组织成线性表 放在内存中,称为段描述符表。分为三类:全局描述符表GDT,局部描述符表LDT和中断描述符表IDT。GDT和IDT在整个系统中只有一张,而每个任务 都有自己私有的一张局部描述符表LDT,用于记录本任务中涉及的各个代码段、数据段和堆栈段以及本任务的使用的门描述符。GDT包含系统使用的代码段、数 据段、堆栈段和特殊数据段描述符,以及所有任务局部描述符表LDT的描述符。

 

GDTR全局描述符寄存器


48位,高32位存放GDT基址,低16为存放GDT限长。

 

LDTR局部描述符寄存器


16位,高13为存放LDT在GET中的索引值。

IA-32处理器仍然使用xxxx:yyyyyyyy(段选择器:偏移量)逻辑方式表示一个线性地址,那么是怎么得到段的基址呢?在上面说明中我们知道,要得到段的基址首先通过段选择符xxxx中TI位指定的段描述符所在位置: 当 TI=0时表示段描述符在GDT中,如下图所示:

  • ① 先从GDTR寄存器中获得GDT基址。
  • ② 然后再GDT中以段选择符高13位位置索引值得到段描述符。
  • ③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

 

当TI=1时表示段描述符在LDT中,如下图所示:

  • ① 还是先从GDTR寄存器中获得GDT基址。
  • ② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。
  • ③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
  • ④ 用段选择符高13位位置索引值从LDT段中得到段描述符。
  • ⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

 

linux中GDT,LDT和IDT结构定义


GDT描述符表gdt_page定义

struct gdt_page {
    struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));

DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);

段描述符结构定义


ia32机器上的定义


定义在arch/x86/include/asm/desc_defs.h文件中

struct desc_struct {
  union {
    struct {
      unsigned int a;
      unsigned int b;
    };
    struct {
      u16 limit0;
      u16 base0;
      unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
      unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
    };
  };  
} __attribute__((packed));

联合体——对成员域访问和设置成为一种很优美的方法。上面第一个匿名结构体用来作为成员访问取值的出口,下面第二个结构体对真实的成员设置值的入口。

字段描述
limit段长度
base段的首字节的线性地址,有base0,base1,base2三部分构成
type段的类型和存取权限
s系统标志。1-系统段;0-普通段
dpl描述符特权级
psegment-Present。linux下总是1
avllinux不用
d区分代码段还是数据段
g段大小粒度。以4K倍数计算

在32位机器上,这就是所有描述符的数据结构喽,没有细分门和非门!

typedef struct desc_struct gate_desc;
typedef struct desc_struct ldt_desc;
typedef struct desc_struct tss_desc;

由于三类描述符都是一个结构类型,从而一律使用下面宏初始化在GDT中表项

#define GDT_ENTRY_INIT(flags, base, limit) { { { \
        .a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \
        .b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \
            ((limit) & 0xf0000) | ((base) & 0xff000000), \
    } } }

x64机器上的定义


但是在64位机器上,Linux则进行了细致划分:

/* 16byte gate */
struct gate_struct64 {
  u16 offset_low;
  u16 segment;
  unsigned ist : 3, zero0 : 5, type : 5, dpl : 2, p : 1;
  u16 offset_middle;
  u32 offset_high;
  u32 zero1;
} __attribute__((packed));

16字节LDT或TSS描述符结构

/* LDT or TSS descriptor in the GDT. 16 bytes. */
struct ldttss_desc64 {
  u16 limit0;
  u16 base0;
  unsigned base1 : 8, type : 5, dpl : 2, p : 1;
  unsigned limit1 : 4, zero0 : 3, g : 1, base2 : 8;
  u32 base3;
  u32 zero1;
} __attribute__((packed));
typedef struct gate_struct64 gate_desc;
typedef struct ldttss_desc64 ldt_desc;
typedef struct ldttss_desc64 tss_desc;

从上面代码看出无论是32位还是64位机器上,都使用typedef重新定义,以提供给系统其他使用此描述符的部分一致的类型名

区分描述符的枚举量

enum {
    GATE_INTERRUPT = 0xE,
    GATE_TRAP = 0xF,
    GATE_CALL = 0xC,
    GATE_TASK = 0x5,
};
enum {
    DESC_TSS = 0x9,
    DESC_LDT = 0x2,
    DESCTYPE_S = 0x10,  /* !system */
};

系统GDT,IDT指针描述结构

struct desc_ptr {
    unsigned short size;
    unsigned long address;
} __attribute__((packed)) ;

这个结构记录了系统的GDT或者IDT的大小以及在系统中的线性基地

 

参考文章


https://blog.csdn.net/billpig/article/details/5833980

https://blog.csdn.net/kunkliu/article/details/102729635

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页