This is a bilingual snapshot page saved by the user at 2025-1-10 8:40 for https://time.geekbang.org/column/article/431400, provided with bilingual support by Immersive Translate. Learn how to save?
编程高手必学的内存知识
海纳
华为编译器高级专家,原 Huawei JDK 团队负责人
21400 人已学习
查看详情
课程目录
已完结/共 33 讲
开篇词 (1讲)
开篇词|为什么你要系统学习计算机的内存知识?
时长 10:43
课程导学 (3讲)
导学(一)| 拆解CPU的基本结构和运行原理
时长 01:09:24
导学(二)| 汇编语言是怎么一回事?
时长 01:56:03
导学(三)| 一个CPU是怎么寻址的?
时长 01:36:30
软件篇 (12讲)
01|虚拟内存:为什么可用内存会远超物理内存?
时长 17:18
02|聊聊x86体系架构中的实模式和保护模式
时长 20:01
03 | 内存布局:应用程序是如何安排数据的?
时长 21:19
04 | 深入理解栈:从CPU和函数的视角看栈的管理
时长 20:07
05 | 栈的魔法:从栈切换的角度理解进程和协程
时长 20:29
06 | 静态链接:变量与内存地址是如何映射的?
时长 24:27
07 | 动态链接(上):地址无关代码是如何生成的?
时长 23:11
08 | 动态链接(下):延迟绑定与动态链接器是什么?
时长 23:00
09 | 深入理解堆:malloc和内存池是怎么回事?
时长 20:47
10 | 页中断:fork、mmap背后的保护神
时长 22:55
11 | 即时编译:高性能JVM的核心秘密
时长 20:02
12 | 内存虚拟化:云原生时代的奠基者
时长 23:37
硬件篇 (5讲)
13 | 存储电路:计算机存储芯片的电路结构是怎样的?
时长 23:45
14 | CPU Cache:访存速度是如何大幅提升的?
时长 21:25
15 | MESI协议:多核CPU是如何同步高速缓存的?
时长 21:00
16 | 内存模型:有了MESI为什么还需要内存屏障?
时长 19:29
17 | NUMA:非均匀访存带来了哪些提升与挑战?
时长 21:57
自动内存管理篇 (7讲)
18 | Java内存模型:Java中的volatile有什么用?
时长 19:30
19 | 垃圾回收:如何避免内存泄露?
时长 17:12
20 | Scavenge:基于copy的垃圾回收算法
时长 20:04
21 | 分代算法:基于生命周期的内存管理
时长 23:36
1%
22 | G1 GC:分区回收算法说的是什么?
时长 20:48
23 | Pauseless GC:挑战无暂停的垃圾回收
时长 20:09
2%
24 | GC实例:Python和Go的内存管理机制是怎样的?
时长 24:44
加餐:不定期福利 (3讲)
不定期福利第一期 | 海纳:我是如何学习计算机知识的?
时长 10:49
不定期福利第二期 | 软件篇答疑
时长 11:30
直播加餐|链接器和加载器是如何为你工作的?(合集)
时长 01:57
结束语 (2讲)
期末测试 | 来赴一场满分之约吧!
时长 00:35
结束语 | 自主基础软件开发的那片星辰大海
时长 11:53
编程高手必学的内存知识
15
15
1.0x
02:22/20:01
按键 P :暂停/播放音频

02|聊聊x86体系架构中的实模式和保护模式

讲述:海纳大小:18.29M时长:20:01
你好,我是海纳。
上一节课我们讲了虚拟内存的概念,分析了线性地址(虚拟地址)是如何映射到物理地址上的。
不过,在 x86 架构诞生之初,其实是没有虚拟内存的概念的。1978 年发行的 8086 芯片是 x86 架构的首款芯片,它在内存管理上使用的是直接访问物理内存的方式,这种工作方式,有一个专门的名称,那就是实模式(Real Mode)。上节课我们也曾简单提到过,直接访问物理内存的工作方式让程序员必须要关心自己使用的内存会不会与其他进程产生冲突,为程序员带来极大的心智负担。
后来,CPU 上就出现虚拟内存的概念,它可以将每个进程的地址空间都隔离开,极大地减轻了程序员的负担,同时由于页表项中有多种权限保护标志,极大地提高了应用程序的数据安全。所以人们把 CPU 的这种工作模式称为保护模式(Protection Mode)。
从实模式演进到保护模式,x86 体系架构的内存管理发生了重大的变化,最大的不同就体现在段式管理和中断的管理上。所以今天这节课,我们会围绕这两个重点,让你彻底理解 x86 体系架构下的内存管理演进。你也能通过这节课的学习,学会阅读 Linux 内核源码的段管理和中断管理的相关部分,还可以增加调试 coredump 文件的能力。
这里我们就按照时间顺序,从 8086 芯片中的实模式开始讲起。

8086 中的实模式

8086 芯片是 Intel 公司在 1978 年推出的 CPU 芯片,它定义的指令集对计算机的发展历程影响十分巨大,之后的 286、386、486、奔腾处理器等等都是在 8086 的基础上演变而来。这一套指令集也被称为 x86 指令集。直到今天,很多大学里的微机原理课和汇编语言课还是使用 8086 进行讲解。
8086 的寄存器只有 16 位,我们也习惯于称 8086 的工作模式是 16 位模式。而且,后面的 CPU 为了保持兼容,在芯片上电了以后,还必须运行在 16 位模式之下,这种模式有个正式的名字,叫做实模式(Real Mode)。在实模式下,程序员是不能通过内存管理单元(Memory Management Unit, MMU)访问地址的,程序必须直接访问物理内存。
那实模式下,我们是怎么访问存储的物理地址的呢?
8086 的寄存器位宽是 16 位,但地址总线却有 20 位,地址的编码可以从 20 位 0 到 20 位 1,这意味着 8086 的寻址空间是 2^20 = 1M。但是在写程序的时候,我们没有办法把一个地址完整地放到一个寄存器里,因为它的寄存器相比地址少了 4 位。
为了解决这个问题,8086 就引入了段寄存器,例如 cs、ds、es、gs、ss 等。段寄存器中记录了一个段基地址,通过计算可以得到我们存储的真实地址,也就是物理地址。物理地址可以使用“段寄存器: 段内偏移”这样的格式来表示,计算的公式是:
物理地址 = 段寄存器 << 4 + 段内偏移
不过,在我们写汇编代码的时候(如果你对汇编不熟悉,可以先去看看我前面讲的导学(一)导学(二)),也不一定就要使用段寄存器来表示段基址,也可以使用“段基址: 段内偏移”这样的立即数的写法,比如你可以看下这个节选自 Linux 的 bootsect 中的代码:
BOOTSEG = 0x7c0
_start:
jmpl $BOOTSEG, $start2
start2:
movw $BOOTSEG, %ax
movw %ax, %ds
...
这块代码里,它跳转的目标地址就是 0x7c0 << 4 + OFFSET(start2)。跳转成功以后,cs 段寄存器中的值就是段基址 0x7c0,start2 的偏移值是 8,所以记录当前执行指令地址的 ip 寄存器中的值就是实际地址 0x7c08。
而且,这块代码里也包含了段基址和段内偏移值这种地址形式,这显然有别于我们所讲的虚拟地址。这种包含了段基址和段内偏移值的地址形式有一个专门的名字,叫做逻辑地址。你可以看到,虚拟地址是一个整数,而逻辑地址是一对整数。所以说,在 8086 芯片中,逻辑地址要经过一步计算才可以得到物理地址。
在 8086 中,cs 被用来做为代码段基址寄存器,比如上面示例代码中的 jmp 指令,跳转成功就会把段基址自动存入 cs 寄存器。ds 被用来做为数据段基址寄存器,你可以看看下面这个代码:
INITSEG = 0x9000
....
movw $INITSEG, %ax
movw %ax, %ds
movb $0x03, %ah
xor %bh, %bh
int $0x10
movw %dx, (0)
movb $0x88, %ah
int $0x15
movw %ax, (2)
上述代码的第 7 行执行 0x10 号 BIOS 中断,它的结果存放在 dx 寄存器中,然后第 8 行,将结果存入内存 0x90000,9 至 11 行再把 0x15 号 BIOS 中断的结果存到 0x90002 处。
在寻址时,我们并没有明确地声明数据段基址存储在段寄存器 ds 中,但是 CPU 在执行时会默认使用 ds 做为数据段寄存器。类似的还有 ss,它是做为栈基址寄存器,当我们在使用 push 指令的时候,要保存的数据会放在 ss:(sp) 的位置。
CPU 没有强制规定代码段和数据段分离,也就意味着,你使用 ds 段寄存器去访问指令,CPU 也是允许的。但在实际编程时,我们还是会把数据和代码分到不同的段里,并且将数据段的起始地址放到 ds 寄存器,把代码段的地址放到 cs 寄存器。这种按功能分段的管理内存方式就是段式管理。关于段式管理和页式管理的对比,我们稍后会加以介绍。
到这里 8086 的实模式,我们已经基本讲完了。8086 是最古老的 x86 芯片,在实模式下,它只能直接操作物理内存,非常不便于编程,这一点,我们在第 1 节课也提到了。接下来,我们把目光转向 x86 体系架构中的保护模式,它是实模式的进一步发展。

i386 中的保护模式

经过十年的发展,x86 CPU 迎来了历史上使用最广泛、影响力最大的 32 位 CPU,这就是 i386 芯片。i386 与 8086 的一个很大的不同,就是它采用了全新的保护模式。这个体现在,i386 中的段式管理机制,相比 8086 发生了重大变化;同时,i386 芯片在段式管理的基础上,还引入了页式管理。
i386 在完成各种初始化动作以后,就会开启页表,从此程序员就不必再直接操作物理内存的地址空间了,代替它的是线性地址空间。而且由于段和页都能提供对内存的保护,安全性也得到了提升,所以这种工作模式被称为保护模式(Protection Mode)。i386 的保护模式是一种段式管理和页式管理混合使用的模式。
至于页式管理,我们第 1 节课已经讲过了,所以这里我们就来看一下相比 8086,段式管理在 i386 上有了哪些变化。
变化一:段选择子和全局描述符表
在 i386 上,地址总线是 32 位的,通用寄存器也变成 32 位的,这就意味着因为寄存器位数不够而产生的段基址寄存器已经失去了作用。
但是 i386 没有直接放弃掉段寄存器,而是将它进化成了新的段式内存管理。段寄存器仍然是 16 位寄存器,但是其中存的不再是段基址,而是被称为段选择子的东西
相比 8086 芯片,i386 中多了一个叫全局描述符表(Global Descriptor Table, GDT)的结构。它本质上是一个数组,其中的每一项都是一个全局描述符,32 位的段基址就存储在这个描述符里。段选择子本质上就是这个数组的下标。具体你可以看看下面这张图:
GDT 的地址也要保存在寄存器里,这个寄存器就是 GDTR,这个做法和第 1 节课我们讲到的 CR3 寄存器的做法十分相似。
在上面这张图中,CPU 在处理一个逻辑地址“cs:offset”的时候,就会将 GDTR 中的基址加上 cs 中的下标值来得到一个段描述符,再从这个段描述符中取出段基址,最后将段基址与偏移值相加,这样就可以得到线性地址了。这个线性地址就是我们第 1 节课中所讲的虚拟地址。
得到线性地址以后,剩下的工作我们就非常熟悉了:由 CPU 的 MMU 将线性地址映射为物理地址,然后就可以交给地址总线去进行读写了。
变化二:段寄存器对段的保护能力增强
在 8086 中,段寄存器只起到了段基址的作用,对于段的各种属性并没有加以定义。例如,在实模式下,任何指令都可以对代码段进行随意地更改。
但在 i386 中,对段的保护能力加强了,我们先来看一下 i386段描述符(也就是 GDT 中的每一项)的结构。
你会看到,描述符中除了记录了段基址之外,还记录了段的长度,以及定义了一些与段相关的属性,其中比较重要的属性有 P 位、DPL、S 位、G 位和 Type。我们接下来一个个来分析。
P 位是一个比特,指示了段在内存中是否存在,1 表示段在内存中存在,0 则表示不存在
DPL,占据了两个比特,指的是描述符特权级,英文是 Descriptor Privilege Level。Intel 规定了 CPU 工作的 4 个特权级,分别是 0、1、2、3,数字越小,权限越高。
以 Linux 为例,Linux 只使用了 0 和 3 两个特权级,并且规定 0 是内核态,3 是用户态。特权级的切换是比较复杂的一种机制,但 Linux 只使用了中断这一种,后面我们会再讲到中断。
接下来我们再看 S 位,S 为 1 代表该描述符是数据段 / 代码段描述符,为 0 则代表系统段 / 门描述符。门是 i386 提供的用于切换特权级的机制,有调用门、陷阱门、中断门、任务门等。在 Linux 系统中,只使用了中断门描述符。
然后是 G 位,它指的是定义段颗粒度(Granularity),它的值为 0 时,段界限的单位是字节,为 1 时段界限以 4KB 为单位,也就是一页。
我们也可以从图中看出定义段长度的“段界限”字段并不是连续的,它一共有 20 位,分散在两个地方。当 G=1 时,段界限的最大值是 2^20 * 4K = 4G,这是 i386 一个段的最大长度。
最后是 Type 属性,它定义了描述符类型,我把比较重要的类型用表列在了下面,你可以看看。
到这里,我们已经解释清楚了,i386 中保护模式相比 8086 实模式在段式管理上的升级。那么在现代的 CPU 和操作系统中,段式管理和页式管理又是怎样的关系呢?要讲清楚这一点就要先对比这两种内存管理方式的优缺点。

段式管理对比页式管理

段式管理会按功能把内存空间分割成不同段,有代码段、数据段、只读数据段、堆栈段,等等,为不同的段赋予了不同的读写权限和特权级。通过段式管理,操作系统可以进一步区分内核数据段、内核代码段、用户态数据段、用户态代码段等,为系统提供了更好的安全性。
但是段的长度往往是不能固定的,例如不同的应用程序中,代码段的长度各不相同。如果以段为单位进行内存的分配和回收的话,数据结构非常难于设计,而且难免会造成各种内存空间的浪费。页式管理则不按照功能区分,而是按照固定大小将内存分割成很多大小相同的页面,不管是存放数据,还是存放代码,都要先分配一个页,再将内容存进页里。
所以,你可以看到,相比页式管理,段式管理的优点是提供更好的安全性,按照内存的用途进行划分更符合人的直观思维。它的缺点就是由于不定长,难于进行分配、回收调度
而页式管理的优点是大小固定,分配回收都比较容易。而且段式管理所能提供的安全性,在现代 CPU 上也可以被页表项中的属性替代,所以现在段式管理已经变得越来越不重要了。像 64 位 Linux 系统,它把所有段的基地址都设成了从 0 开始,段长度设置为最大。这样段式管理的重要性就大大下降了。
但是,如果我们以 x86 的历史演进来看,你会发现段式管理其实是最早出现的(8086 芯片),然后才出现了页式管理(i386 芯片)。而且,我们现代的 x86 架构的 CPU,也同时兼容段式管理和页式管理,我们可以认为是一种混合的段页式管理(当然,并不是所有人都认可这种命名方式)。
总的来说,现代的操作系统都是采用段式管理来做基本的权限管理,而对于内存的分配、回收、调度都是依赖页式管理。
到这里,我们就讲清楚了 8086 实模式到 i386 保护模式下段式管理的演进,并且进一步分析了段式管理和页式管理的对比和现状。
保护模式相比实模式,发生重大变化的不止是内存管理,同时还有中断管理。因为管理中断的结构与段式管理的全局描述符表的结构非常相似,所以我们在讲保护模式时也一起讲一下。你可以将中断机制与段管理机制比较着一起学习。

中断描述符表

中断描述符表(Interruption Description Table, IDT),是 i386 中一个非常重要的描述符表,它也是保护模式对比实模式的另一大不同。你在后面学习 fork、execve 的实现时,涉及到的写保护中断,缺页中断等机制都要依赖它。
CPU 与外设之间的协同工作是以中断机制来进行的。例如,我们敲击键盘的时候,键盘的控制器就会向 CPU 发起一个中断请求。CPU 在接到请求以后,就会停下正在做的工作,把当前的寄存器状态全部保存好,然后去调用中断服务程序。当然,这个过程中有一些是 CPU 的工作,有一些是操作系统的工作,但因为我们关注的重点是内存,所以就没必要计较这里面细微的差别了。
中断根据中断来源的不同,又可以细分为 Fault、Trap、Abort 以及普通中断。我们这门课对它们也不加区分,例如执行除法的时候除数为 0 的情况、访问数据时权限不足引发的保护错误、由用户使用 int 指令产生的中断等,虽然中断源不同,它们的类型也不相同,但我们统一称它们为中断。
硬件负责产生中断,CPU 会响应中断,但是中断来了以后要做什么事情是由操作系统定义的。操作系统要通过设置某个中断号的中断描述符,来指定中断到达以后要调用的函数中断描述符表(IDT)的作用就体现在这了,它的本质就是中断描述符的数组
IDT 的基地址存储在 idtr 寄存器中,这和 GDTR 的设计如出一辙。每个中断都有一个编号与其对应,我们称之为中断向量号中断向量号是 CPU 提前分配好的,我也把比较重要的中断向量号放在了下表里,你可以看看。
在这个表里,我们没有看到前边所提到的键盘中断,这是因为键盘中断都是由一个名为 8259A 的芯片在管理。
两片级联的 8259A 芯片可以管理 16 个中断,其中包括了时钟中断、键盘中断,还有软盘、硬盘、鼠标的中断等等。这些中断的中断向量号是可以通过对 8259A 编程进行设置的。虽然 8259A 的编程比较繁琐,但好在只需要操作系统开机引导时设置一次。
你也可以看到,Linux 系统把中断向量表的 32 号中断(用户自定义中断的第一位)设置成 8259A 的 0 号中断,也就是说 IDT 的 32 号至 47 号都分配给了 8259A 所管理的中断。键盘、软盘、硬盘、鼠标的中断服务程序就设置在这里。
关于中断,我们掌握这么多就已经足够了,更多的知识我们会在后面的课程按需讲解。
现在,我们可以通过一个例子,体验一下中断的使用。在 Linux 系统上,我们把下面这个代码保存到文件 hello.c 中,并且使用"gcc -o hello hello.c"编译,得到可执行程序 hello。再运行它,你就可以看到屏幕上打印出一行"hello"。
// compile command : gcc -o hello hello.c
void sayHello() {
const char* s = "hello\n";
__asm__("int $0x80\n\r"
::"a"(4), "b"(1), "c"(s), "d"(6):);
}
int main() {
sayHello();
return 0
}
相比于使用 printf 进行打印,需要引入头文件"stdio.h",我们这段代码里没有使用任何头文件,但一样可以在控制台上进行打印。
这是因为,我们使用了 0x80 号中断进行了 Linux 系统调用。系统调用号在 eax 中,也就是 4,代表 write 这个调用。第一个参数在 ebx 中,其值为 1,代表控制台的标准输出;第二个参数是字符串"hello"的地址,在 rcx 中;第三个参数是字符串的长度,也就是 6,存储在 edx 中。
这样,我们就通过中断,就不必再使用 C 语言的 printf 进行输出,这就绕过了 C 语言的基础库,完成了向控制台打印的功能。

总结

今天我们拆解了 x86 体系架构下的实模式和保护模式,也认识了两个 x86 演进史上非常重要的 CPU。
8086 是 16 位的 CPU,我们称 8086 的工作模式为实模式,它的特点是直接操作物理内存,内存管理容易出错,要十分小心,代码编写和调试都很困难。
之后出现的 i386,则采用了和实模式不同的保护模式。相比实模式,i386 中的保护模式,采用了页式管理,但它没有彻底放弃 8086 的段式管理,而是将段寄存器中的值由段基址变成了段选择子。段选择子本质是 GDT 表的下标值,段基址都转移到 GDT 中去了。
段式管理负责将逻辑地址转换为线性地址,或者称为虚拟地址,页式管理负责将线性地址映射到物理地址。i386 的保护模式采用了段页式混合管理的模式,兼具了段式管理和页式管理的优点。
除了段页式内存管理这个不同之外,保护模式和实模式的区别还体现在中断描述符表(IDT)上。IDT 是保护模式的一个重要组成部分,它保存着 i386 中断服务程序的入口地址。
8086 和 i386 对 x86 架构的 CPU 影响巨大。直到今天,x86 架构的 CPU 在上电以后,为了与 8086 保持兼容,还是运行在 16 位实模式下,也就是说所有访存指令访问的都是物理内存地址。在启动操作系统后,才会切换到保护模式下进行工作。

思考题

我们今天这节课只讲了 16 位 CPU 和 32 位 CPU,并没有讲 64 位 CPU 的段式管理是怎么做的。实际上 64 位 CPU 的段式管理和 32 位的结构非常相似,惟一的区别是段描述符的段基址和段长度字段都被废弃了,也就是说不管你将段基址设置成什么,都会被 CPU 自动识别为 0。
那么请你思考,CPU 为什么要这么设计呢?一方面,它还保留了段寄存器,另一方面,它又不再起到逻辑地址转换线性地址的作用,这不是很奇怪吗?请你站在 CPU 架构师的角度思考一下原因。欢迎你在留言区分享你的想法和收获,我在留言区等你。
好啦,这节课到这就结束啦。欢迎你把这节课分享给更多对计算机内存感兴趣的朋友。我是海纳,我们下节课再见!
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
  • 解释
  • 总结

x86架构中的实模式和保护模式是本文的主题。文章首先介绍了8086芯片的实模式,该模式下程序员需要直接访问物理内存,使用段寄存器来计算物理地址。然后,文章提到了保护模式的出现,它引入了虚拟内存的概念,隔离了每个进程的地址空间,减轻了程序员的负担,并提高了数据安全性。接着,文章详细讲解了8086中的实模式,包括寄存器位宽、段寄存器的作用、逻辑地址和物理地址的计算方式等。最后,文章提到了保护模式是实模式的进一步发展,带来了重大的内存管理变化,包括段式管理和中断的管理。 在i386芯片中,保护模式引入了全局描述符表(GDT)和页式管理,使得段式管理发生了重大变化。段寄存器进化成了段选择子,而GDT中的每一项都是一个全局描述符,存储了32位的段基址。保护模式的段描述符中记录了段的长度和属性,如P位、DPL、S位、G位和Type,增强了对段的保护能力。与此同时,文章还对比了段式管理和页式管理的优缺点,指出现代操作系统采用段式管理进行基本权限管理,而依赖页式管理进行内存的分配、回收和调度。 总的来说,本文详细介绍了8086实模式到i386保护模式下段式管理的演进,以及段式管理和页式管理的对比和现状。保护模式不仅带来了内存管理的重大变化,还影响了中断管理。读者通过本文可以全面理解x86体系架构下的内存管理演进,以及阅读Linux内核源码的相关部分和增强调试能力。

2021-10-2724人觉得很赞给文章提建议

上一篇
01|虚拟内存:为什么可用内存会远超物理内存?
下一篇
03 | 内存布局:应用程序是如何安排数据的?
BessieChen
Command + Enter 发表
0/2000字符
提交留言

全部留言(27)

  • 最新
  • 精选
  • 慢动作
    没有段以后,代码权限是以什么为单位管理的?GDT是每个进程单独一份,IDT是系统独一份?

    作者回复: 1. 第一个问题,linux内核引入了vm_area_struct结构,通过软件的办法做了很多权限管理的事情,这个可以代替段的权限管理的部分工作;另外,每个页也有读写权限管理,所以硬件本身的页管理机制也代替了一些段的权限管理机制。 2. IDT是全局的,你已经理解了。GDT也是全局的。linux会使用GDT来区分内核代码段和内核数据段。每个进程单独的确实也有这种结构,叫做局部(local)表述符表,LDT才是和单个进程相关的表,其中的描述符的结构与全局描述符是完全一样的。由于它的结构和GDT非常像,我故意略去了。 3. 段选择子我没有展开讲它的结构,实际上,段选择子的第三位为0就表示要在GDT中找描述符,为1就在LDT中查找。所以你只要理解GDT就足够了。但既然你考虑到这里了,我就单独回答一下这个问题。 你思考得很深入,点赞!

    2021-10-27
    3
    20
  • Yun
    “实际上 64 位 CPU 的段式管理和 32 位的结构非常相似,惟一的区别是段描述符的段基址和段长度字段都被废弃了,也就是说不管你将段基址设置成什么,都会被 CPU 自动识别为 0”。 这句话里的“CPU自动识别为0”?记得是Linux内核将段基地址都设置为0,然后是的Linux本质上抛弃了段式管理,所以想请教一下,这里是CPU将段基址设置为0还是Linux内核来设置?或者说32位和64位还不一样?,谢谢

    作者回复: 32位linux先动的手,intel一看,你们写os的不按套路出牌啊,所以就干脆在设计64位cpu的时候把这块电路给扣了。

    2022-02-14
    7
  • 郑童文
    请问老师,IDT是储存在操作系统的内核内存空间的吗?GDT是存在进程的用户内存空间还是内核内存空间呢? 谢谢!

    作者回复: 好问题。gdt和idt都是由操作系统设置的。所以我们可以理解成是在内核空间里。但这仍然不准确。最准确的说法是由于gdtr和idtr里存的是物理地址,相当于操作系统从物理地址里扣了一块给gdt和idt。这块物理地址以后就不参与分配了。

    2021-10-27
    5
    7
  • 送过快递的码农
    保留段寄存器是不是为了向下兼容实模式啊?因为64位地址总线够大了,不需要段了。但是为了保证向下兼容性,段不做删除?纯属瞎猜

    作者回复: Good,这是很重要的一方面。是x86的设计哲学,但也给x86架构带来了巨大的包袱。

    2021-10-27
    5
  • 『SIGNIFICANT』
    java程序员一般不会接触到coredump这样的文件分析,一般也就是JVM调优,内存信息dump下来用工具查看,还有必要深入了解这些么

    作者回复: 不需要。java程序员的成长建议先学java类库,把算法和数据结构吃透是收益最大的。

    2021-12-06
    4
    4
  • .
    两顿饭钱买到很多知识,期待后面内存模型那块。以前看文章说理解x86内存模式是强类型,所以有部分标准内存屏障是不存在,我一脸懵了好多年。

    作者回复: OK,请继续关注,我们一定能讲清楚内存屏障是干嘛的。

    2021-11-15
    3
  • keepgoing
    老师,能理解为之后的i386中主要用段式管理(也就是GDT)来管理段类型的区分,比如代码段/数据段,用页式管理(所以GDT中的描述子G值一般都为1)来管理真正的物理内存和虚拟内存的映射吗? 如果这样来说的话,我理解段式管理主要来作类型的区分,在i386中程序员自己会去用逻辑地址寻址的场景是否会比较少,主要是什么场景会用到逻辑寻址呢。 初学小白,听了老师的课比较好奇,如有冒犯或理解不对的地方请老师多多包涵

    作者回复: 你的理解是对的。cpu的设计者是希望大家还继续使用段机制的,但是linux不按套路出牌,弱化了段的作用。linux主要使用页管理,所以,是的,后来的程序员不再使用逻辑地址了。

    2021-11-03
    4
    3
  • coder
    老师给的例子在x86上可以跑通,arm上不行

    作者回复: 你说得非常对!看了我们的前导课就会明白,x86和arm的寄存器都不一样,所以这个内嵌汇编只能在x86上运行。汇编是不能跨平台的。不过你倒是可以尝试修改一下哦,这就是跨架构移植了:)

    2021-10-27
    3
  • =
    内存碎片以前理解时,只考虑到了段式内存管理会有段间的内存碎片,一直没有考虑到页式内存管理的页内部的内存碎片问题。 海老师的这篇文章,从“16位CPU演化到32位CPU后内存管理的变化”的角度讲起,帮助我更好地理解了全局段描述符的问题,之前看x86CPU的GDT等知识,不知其所以然,但是今天从内存管理的角度来看,很清晰!

    作者回复: good。带着思考去阅读收获才能更大

    2022-01-10
    1
  • 乘风
    段式管理和页式管理都是针对的物理内存是吧,很虚拟内存没关系

    作者回复: 不是。它们都是内存管理的一种思路,既可以用于物理内存也可以用于虚拟内存。你可以这样理解虚拟内存:它和物理内存一样,也是一段地址空间,只是它还需要通过页表进行一次映射而已。对内存的管理,本质上就是对一段地址区间进行管理。再想想?

    2022-01-07
    1
  • 牙齿天天晒太阳
    8086 的寄存器位宽是 16 位,但地址总线却有 20 位 ------------------------------- 为什么不让寄存器位宽和地址总线相等呢?

    作者回复: CPU设计的时候就是这样的,这个我也不知道了。40年前,也许是硬件规格限制了,也许是制造工艺限制了,我们也很难考证了。

    2022-01-03
    1
  • 稽山的柚子
    跳转成功以后,cs 段寄存器中的值就是段基址 0x7c0,start2 的偏移值是 8,所以记录当前执行指令地址的 ip 寄存器中的值就是实际地址 0x7c08。 请问老师 跳转成功以后cs段寄存器的值为什么是0x07c0呢?没有道理呀

    作者回复: 因为jmp指令的目标地址写的就是段基址,段内偏移这样的格式呀。你加微信群了吗?我在微信群里发过一个视频,讲实模式怎么实验的。你可以加一下,微信上问下。

    2021-12-16
    2
  • .
    老师我不知道我理解的对不对。段页式管理就是将GDT设置为1并且设置GDT为最大的长度为4G.然后再页内通过操作系统手动再去细化内存的为段管理方式? 假设我电脑8G 使用段页式管理。linux操作系统会有两个GDT? 假设我电脑只有4G使用段页式管理那么GDT只有一个?如果只有一个GDT如何区分内核内存区域和用户态内存区域?因为我看文章GDT是可以标志出这块内存是否为内核内存

    作者回复: GDT是一个表,这个表里是有很多项的,也有就是有很多全局描述符。表只有一个哈。我们先来对齐一下这个概念。然后Linux为每个进程都准备了各种段描述符,有的是全局的,有的是局部的。它们都被映射到了0~4G这个区域,但是权限不同。仅此而已。所以你看,这已经是一个不重要的概念了。你可以搞任意多个去描述内核数据段,内核代码段,内核堆栈段,这不是一个固定的机制。

    2021-11-16
  • 小时候可鲜啦
    “X86 架构的 CPU 在上电以后,为了与 8086 保持兼容,还是运行在 16 位实模式下”,这句话的意思是用户模式下的cpu处于下电状态?下电不是不工作了吗

    作者回复: 下电状态是指关机。上电可以理解为开机。这句话的意思是,CPU在开机以后先工作在实模式下,需要操作系统来负责进入保护模式。

    2021-11-05
  • HollyWong
    64位保留段寄存器主要应该是兼容考虑,IA64不兼容32位x86已经得到教训了。
    2021-11-10
    3
  • HollyWong
    碎片问题就是外碎片和内碎片的问题
    2021-11-10
    3
  • lanco
    请教一下老师,"现代的操作系统都是采用段式管理来做基本的权限管理,而对于内存的分配、回收、调度都是依赖页式管理。",这句话的理解,linux目前是将段的起始地址都设置为0,也就是段都是同一个了,还怎么做基本的权限管理;另外,根据上面所说,段页式,我理解其实就是页式管理,因为段都指向同一块地址了
    2022-11-05归属地:北京
    1
  • Spoon
    GDT的具体事例可以放一个,光说概念有点空洞;IDT在操作系统里面应该就是一个数组
    2022-10-02归属地:浙江
    1
  • 满分💯
    【8086 的寄存器位宽是 16 位,但地址总线却有 20 位】 这个设计是由于当时的技术限制,导致寄存器和地址总线位数不相同吗,有没有什么有趣的历史?
    2021-12-04
    1
  • Geek_d797a2
    老师好,我从远程终端连接到一台 ubuntu 的机器,内联汇编的例子却没成功。我用 gcc -S 汇编,发现内联的汇编代码都有的,为什么没在终端上打印呢? 还有啊,我发现汇编出来的代码,会先把那个ebx需要的参数 1 放在 esi 里,然后再 movl %esi, %ebx ,这又是什么原因呢?
    2021-12-02
    1
    1
  • 群书
    这块代码里,它跳转的目标地址就是 0x7c0 << 4 + OFFSET(start2)。跳转成功以后,cs 段寄存器中的值就是段基址 0x7c0,start2 的偏移值是 8,所以记录当前执行指令地址的 ip 寄存器中的值就是实际地址 0x7c08。 跳转成功后cs段寄存器的值应该是0x7c00吧
    2023-11-14归属地:山东
  • 机器人
    思考题 64位CPU 段基址 被识别0 感觉是 因为地址空间足够大 就如一个国家的IP地址一样,如果可被分配的公网IP足够多 局域网就没必要了 保留则是为了兼容
    2023-11-09归属地:北京
  • Geek_zbvt62
    "CPU 没有强制规定代码段和数据段分离,也就意味着,你使用 ds 段寄存器去访问指令..." 这一段话我没能理解。 INITSEG = 0x9000 .... movw $INITSEG, %ax movw %ax, %ds movb $0x03, %ah xor %bh, %bh int $0x10 movw %dx, (0) movb $0x88, %ah int $0x15 movw %ax, (2) 在这段代码中,我猜测movw %dx, (0)这种是因为CPU知道是一个移动数据的指令,所以会采用%ds内的值,左移四位后,作为段基址。这种情况下如何能换为%cs寄存器呢?
    2023-08-26归属地:天津
  • gover
    hello的例子return 0处少了一个分号
    2023-02-28归属地:广东
  • 会爆炸的小米Note
    老师好 如果页式管理在虚拟内存层面考虑是不是就既有内部碎片也有外部碎片了呢
    2022-03-05
  • Y X
    请问作者,我目前是大四科班应届生以后也是打算从事java 方向,买了这门课 目的也是打算了解一些底层相关的知识,那我应该优先看哪些部分呢。
    2022-03-03
  • Horizon
    老师请问一下示例编译报这个错误是什么原因啊 hello.c:13:1: 错误:expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ at end of input }I ^
    2022-02-07
收起评论
大纲
取消固定
8086 中的实模式
i386 中的保护模式
段式管理对比页式管理
中断描述符表
总结
思考题
退出沉浸式阅读