在上一篇著述中,咱们还是班师的从实花样Private,过渡到了保护花样。
保护花样与实花样最本色的折柳等于:保护花样使用了全局形容符表,用来保存每一个方法(bootloader,操作系统,应用方法)使用到的每个段信息:启动地址,长度,以过甚他一些保护参数。
这篇著述,咱们来看一下 bootloader 是怎样来进行自我进化到保护花样的,然后潜入看一下保护花样是怎样对内存进行安全保护的。
作为布景学问,咱们先来看一下 x86 中的地址变换经由:
x86 处理器中的分页机制是不错被关闭的,此时线性地址就等于物理地址,这亦然咱们一直商议的情况。
下一篇著述,咱们就把 x86 中的分页机制大开,并与 Linux 中的分段和分页机制进行对比。
实花样:bootloader 为方法缠绵段的基地址在之前的著述:Linux从新学06:16张结构图,透澈交融【代码重定位】的底层旨趣中,咱们商议了 bootloader 是怎样把应用方法读取到内存中,临了跳入到方法的进口地址的。
这里所说的方法,不错是操作系统,也不错是应用方法。
底下这张图,是方法被加载到内存中之后,header 中的信息:
因为方法是被 bootloader 动态读取到内存中的,它是不知谈我方被放在内存中的什么位置,因此它也不知谈我方代码段、数据段、栈的启动地址。
然而,方法要想大概平方扩充,就必须要知谈这些信息,那何如办?
唯有 bootloader 才略惩办问题,因为是它来把方法从硬盘加载到内存中的。
因此,bootloader 在跳入方法的进口地址之前,必须把其中的代码段、数据段、栈段的基地址缠绵出来,然后写入到方法的 header 中,如下图所示:
这么的话,方法启动扩充时,就不错从我方的 header 中获得到这 3 个段基地址,况且赋值给相应的寄存器,从而班师的扩充方法。
也等于说:方法的 header 空间,充任了 bootloader 与它进行信圮绝互的序论,用来传递 3 个段寄存器的基地址。
以上的这个经由,一直责任在实花样,因此就莫得段形容符什么事情。
在以后著述中,咱们还会看到在保护花样下,bootloader 仍然会行使 OS 的 header 空间,来传递段的索引号。然后 OS 行使这个段索引号,口交做爱专题去查找 GDT 表,从而找到每一个段的基地址以过甚他一些保护信息。
保护花样:bootloader 为我方创建段形容符bootloader 从 BIOS 接收系统之后,刚启动是运行在实花样下的。
当它完成一些准备责任之后,就不错参加保护花样了,也等于把 CR0 寄存器的 bit0 诞生为 1。
这个准备责任中,最紧迫的等于:竖立 GDT 这个表,况且把 GDT 的启动地址,存储到寄存器 GDTR 中。
底下这张图,是 bootloader 被加载到内存中的布局图:
bootloader 被加载到 0x0000_7C00 地址处。
它最少需要创建 3 个段形容符:代码段、数据段和栈段。
详情 GDT 的地址在创建段形容符之前,需要先详情: 把 GDT 表放在内存中的什么位置?
暂且就把它放在 0x0001_0000 这个地址吧,距离零地址 64K 的位置。
按照处理器的条目,在第 1 个表项(称之为 item 或者 entry,每本书上王人不同样)必须为空形容符(index = 0)。
创建代码段形容符bootloader 的代码放在 0x0000_7C00 启动的地址,长度是 512B。
凭据这些信息,就不错构造出代码段的形容符了:
创建数据段形容符bootloader 待会需要把操作系统或其他应用方法,从硬盘读取到内存中,举例:读取到 0x0002_0000 的位置。
那么 bootloader 就必须大概拜访到这个位置,况且是以数据段的读写样式。
为了行使一齐的 4G 内存空间,bootloader 不错把这 4G 空间,作为一个数据段来界说它的形容符,如下:
创建栈段形容符表面上,bootloader 不错使用内存中的纵情一块闲散空间,来作为我方的栈。
因为栈在 push 操作的工夫,是向低地址观念增长的。
因此好多书本王人会把栈顶基地址诞生为 bootloader 的启动地址,也等于 0x0000_7C00 地址处,况且把栈的空间大小松手在 4K 的规模。
凭据以上这些信息,就不错创建出栈的段形容符,如下:
当以上这几个段的形容符王人创建好之后,就不错把 GDT 的地址(0x0001_0000),诞生到 GDTR 寄存器中了。
临了,再把 CR0 寄存器的 bit0 诞生为 1,就崇拜的参加保护花样来扩充 bootloader 中背面的代码了。
段形容符是怎样确保段的安全拜访的? 段寄存器高速缓存参加保护花样之后,诚然对段寄存器中内容的阐述改革了,然而扩充每一条辅导,照旧需要使用到这些段寄存器的: cs, ds, ss等等。
联想一下:每扩充一条辅导,王人会从逻辑地址中,获得到段索引号,然后去查找 GDT 表,从而定位到段的基地址。
民众王人知谈方法有个“局部性”旨趣,也等于运动扩充的代码,王人是围聚在一段运动的方法空间中的。
这个运动的方法空间,它们王人是在合并个代码段中,因此段的基地址王人是交流的,那么它们王人属于 GDT 中合并个代码段形容符所代表的段空间。
淌若每一条辅导王人去查表,就会影响到方法的扩充后果。
是以,处理器里面就为每一个段寄存器,安排了一个高速缓存。
拿代码段寄存器 cs 来说:当扩充一条辅导的工夫,淌若它与上一条辅导中的段索引号不同,才会凭据新的段索引号到 GDT 中查找相应的段形容符表项。
查找到之后,就把这个表项的内容复制到 cs 寄存器的高速缓存中。
当继续扩充背面的辅导时,淌若逻辑地址中的段索引号莫得变化,处理器就顺利从高速缓存中读取段形容,从而幸免了查表操作,升迁了系统后果。
对段寄存器自身的保护当逻辑地址中段寄存器的索引号改革时,就会凭据新的索引号,到 GDT 中去查表。
天然了,这个索引号不行跳动 GDT 的界限。
当定位到某一个形容符表项之后,就启动进行一系列查验。
再来看一下每一个段形容符中 8 个字节的内容:
bit8 ~ bit11 界说了现时这个段的类型。
耳光 调教假如: 咱们在切换代码段空间的工夫,不防备犯错,定位到了 GDT 中的一个数据段形容符表项,那么处理器就大概实时发现:
“现时这个段形容符的类型是数据段,你却把它算作念代码段来使用,谢却,杀无赦!”
因此,处理器就会隔绝把这个段形容符复制到代码段的高速缓存中,从而对代码段寄存器进行了保护。
对段界限的查验在通过了第一层的段类型保护之后,还会继续对段的界限进行查验,这就要使用到逻辑地址中的偏移地址( EIP )了。
淌若偏移地址跳动了形容符中规则的界限,那么就阐述发生空幻了。
举例:在 bootloader 的代码段形容符中,最大的界限是 512B,淌若把 EIP 诞生为 0x0000_1000,那就信托空幻了。
因为这个地址根柢就不属于代码段的空间规模。
关于数据段来说相比挑升念念,因为咱们把数据段形容符的基地址诞生为 0x0000_0000,段的界限是所有这个词 4G 的空间,是以它不错对所有这个词内存进行操作。
多想一步:
代码段亦然属于这 4G 空间,因此不错通过数据段,来改写代码段空间中的辅导内容。
也等于说:淌若你想修改代码段的辅导,顺利通过代码段来操作是不不错的。
因为代码段形容符中规则了:代码段的内容只可被读取、扩充,然而不行被写入。
此时,就不错别具肺肠:代码段也放在 4G 的空间,那么就不错通过数据段的可写特点,来改写代码段中的辅导。
Private
想一想 gdb 的调试经由,是不是就行使了这个趣味趣味?