Linux分段

2024-06-08 13:48
文章标签 linux 分段

本文主要是介绍Linux分段,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

linux中的分段  

2012-11-04 19:33:37|  分类: kernel-pm |举报 |字号 订阅

x86的内存寻址大家都懂,为了兼容以前的产品,intel保留了段机制,然而linux中弱化了这一机制。下面先说下段机制的历史:

早在8086的时候cpu的地址总线是20根,这样本可以对2^20=1M的地址空间进行寻址,但是由于其数据总线的位宽以及提供段内偏移地址的寄存器位宽只有16位,造成了8086只能最大寻址到2^16=64k的尴尬局面。为了解决这个问题,intel在8086中加了4个段寄存器,分别是我们熟知的CS, DS, SS, ES,并用于代码段,数据段,堆栈段和其他段。同时添加了地址加法器,这使得寻址范围得以扩大,这也是分段机制的由来。不过,这只是时模式下的分段机制,在80286时代,intel引入了保护模式,内存访问收到了级别的限制。然而,80286的地址总线虽然扩大到了24位,但是其数据总线和段内偏移地址寄存器为了兼容前面的芯片依旧保持16位,因此286的段内偏移依旧限制在64k。但是到了386时代,cpu的数据总线和段内偏移地址寄存器达到了32位,这使得cpu的寻址能力扩大到了4G空间。cpu的寻址能力达到4G,但是intel为了兼容前面的产品依旧保留了段机制。这就是段机制的由来和延续至今的原因。

下面主要介绍linux中保护模式下的分段机制。
在32位的cpu中有6个段寄存器,分别是cs,ss,ds,es,fs和gs。其中cs用作代码段寄存器;ss用于栈段寄存器;ds用于数据段寄存器。
剩下的3个段寄存器用于其它用途,可以指向任意数据段。这些段寄存器都是16位寄存器,在其中存放的是段选择符,段选择符的格式如下图所示:
linux中的分段 - yangfan876@126 - yangfan876@126的博客

其中RPL记录该段的特权级,TI是表指示器,用于指示是全局描述符表还是局部描述符表。
下面我写了一个很简单的hello world程序用以查看linux中这几个段描述符中所存的段选择符到底是什么。
代码如下:

#include<stdio.h>

void main (void)

{

printf ("hello world!\n");

}

编译之后在gdb中运行,用info registers查看其寄存器的值,情况如下:
linux中的分段 - yangfan876@126 - yangfan876@126的博客
 
可以看到CS的值为0x73,而其他三个主要的段寄存器都被置为0x7b对应的十进制数分别是115和123。大家可能会奇怪为什么是这样的,其实在linux中是这样定义的__USER_CS和__USER_DS的:

189 #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS*8+3) 190 #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS*8+3)

而GDT_ENTRY_DEFAULT_USER_CS以及 GDT_ENTRY_DEFAULT_USER_DS的定义是这样的:

74 #define GDT_ENTRY_DEFAULT_USER_CS 14 75 76 #define GDT_ENTRY_DEFAULT_USER_DS 15

这样可以算得__USER_DS的值为8*15+3=123, 而__USER_CS的值为8*14+3=115和上面我们在程序中看到的值是一样的。那么__USER_CS对应的二进制数为:0000000001110011可以看出__USER_CS的RPL=3,TI=0,而索引号为14。同理__USER_DS的二进制数为:0000000001111011所以其RPL=3,TI=0,索引号为15。

在内核中也定义了__KERNEL_CS和__KERNEL_DS

187 #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS*8) 188 #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS*8)

其中GDT_ENTRY_KERNEL_CS和 GDT_ENTRY_KERNEL_DS的定义如下

78 #define GDT_ENTRY_KERNEL_BASE (12) 79 80 #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE+0) 81 82 #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE+1)

由此可以算得__KERNEL_CS的值为96而__KERNEL_DS为104,其对应的二进制数为0000000001100000和0000000001101000这样可以看到其RPL=0,TI=0在GDT中的索引号分别为12和13.
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
但是这里有个非常奇怪的问题,当我使用内核模块和添加系统调用的方法分别打印出__KERNEL_CS和__KERNEL_DS时,发现ds寄存器的值并不是__KERNEL_DS所定义的值,而是用户态__USER_DS的值。尝试去解决,但是目前还没有搞定,先mark在这,留待以后再更新。
这里之前和redhat的一个程序员讨论过,安他的意思是2.6以后的内核就开始用__USER_DS了,原因是这样的访问首先在权限上是么有问题的,然后在安全性方面也是没有问题....然后就用了这个...
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

在这里有必要提出关于这几个寄存器的情况,大家看到的资料上都会讲这几个寄存器是16位寄存器,如果大家去看ULK上面会讲到在x86中有一种非编程寄存器用作缓存段描述符然后他给出的图如下:
linux中的分段 - yangfan876@126 - yangfan876@126的博客


 大家看到的非编程寄存器是适合段寄存器分开的,这样容易造成误解,其实在intel文档中是这样的描述的:
linux中的分段 - yangfan876@126 - yangfan876@126的博客
 大家可以看到所有的段寄存器都有可见部分和隐藏部分,当可见部分被载入时隐藏部分也会相应的载入段描述符表中相应的信息,这样在下一次取出信息时就不用再访问内存了,提高了效率。下面是intel文档的原文描述:
Every segment register has a “visible” part and a “hidden” part. (The hidden part is sometimes referred to as a “descriptor cache” or a “shadow register.”) When a segment selector is loaded into the visible part of a segment register, the processor also loads the hidden part of the segment register with the base address, segment limit, and access control information from the segment descriptor pointed to by the
segment selector. The information cached in the segment register (visible and hidden) allows the processor to translate addresses without taking extra bus cycles to read the base address and limit from the segment descriptor. In systems in which multiple processors have access to the same descriptor tables, it is the responsibility of software to reload the segment registers when the descriptor tables are modified.
If this is not done, an old segment descriptor cached in a segment register might be used after its memory-resident version has been modified.

下面介绍段描述符,段描述符存放在段描述符表中,由8个字节组成,其中包含了段基质base,最大偏移量limit以及一些控制信息,这些将在下面的文章中详细介绍。

intel给出的段描述符的结构如下:
linux中的分段 - yangfan876@126 - yangfan876@126的博客
可以看出,再8个字节的段描述符表中用0~3字节中的0~15位以及4~8字节中的16~19位一共20位用来表述段的最大偏移量Segment Limit(共20位),用0~3字节中的16~31位以及以及4~8字节中的16~19, 24~31位表示段基址Base(共32位)。
剩下12位的其他信息含义如下:

G:粒度,当G置为0时,此段的大小范围将在1字节到1M字节的范围内;如果G置为1的话此段的范围将在4字节到4G字节的范围内。
(为什么会这样?我的想法是:G就相当于最大段偏移量的单位,如果G为0,则其单位为1字节,如果为1则段偏移量的单位为4字节。这样段最大偏移量再0001H到FFFFH的范围变化其段的大小将在1byte~1Mbyte或4kbyte~4Mbyte之间变化。或者最大段偏移量的计算公式:当G为0时LIMT+1H;当G为1时LIMT*4K+FFFH。究竟时limit的范围在0001H~FFFFH之间还是在0000H~FFFFH之间有待研究)。

D/B:这个位再不同的段中含义不一样,但有一个统一的含义,如果该位是1则表示该段为32位段,如果是0则是16为段,为了与以前的cpu进行兼容。比如:
1)当此段为代码段描述符时,D=1表示该段使用32位地址级32位或8位操作数;当D=0时表示该段使用16位地址,以及16位或8位操作数。            2)当此段为向下拓展数据段时,D=1表示该段上限为FFFFFFFFH(4G),如果是0表示该段上线为FFFFH(64K)。
3)当此段为栈段时,如果D=1表示该栈段使用32位指针,并且其指针存放在ESP寄存器中;如果为0,表示该栈段使用16位指针,其指针存放在SP寄存器中。如果该栈段在向下拓展数据段中该标志位同时也表示栈的上限,同数据段一样。
L(仅限于代码段):这个标志位如果被置1则表示该代码段在64位模式下执行,如果是0表示再32位兼容模式下执行。如果L位被置1那么D位就要被清除,否则再意义上就会有冲突。如果再32位模式下或者该段不是代码段那么该位无用,需被置0。

 AVL:Available and reserved bits。时预留给操作系统使用的,但被linux忽略了。

P:当该段在内存中的时候此标志位被置为1否则置为0.如果该段被装载如段寄存器中并发现该段的P标志位为0则cpu抛出一个异常,操作系统将从硬盘中把这个段交换到内存中。因为linux中使用了纯分页机制进行虚拟内存管理,所以在linux系统中该位永远被置为1。
(PS:以前想过一个问题,如果一个操作系统使用段机制,如何进行多任务管理。当看到这个标志位的时候瞬间明白了。类似于分页机制系统可以把不同程序的段装载的内存中,但是cpu每次只能运行一个进程,所以可以将当前正在运行的程序的相关段装载入内存。如果进行进程的切换便将现在的段对换入硬盘的交换分区,用这样的方式实现虚拟内存管理是可行的。但是这样的方式再进程切换的效率上是极低的,因为如果进行进程切换就将进行段的换入换处操作;而且这种虚拟内存的管理运用在多处理机的cpu上就会出现不同进程在物理地址的映射上发生冲突的事情。这可能也是大多造作系统抛弃段机制,而使用纯的分页机制进行虚拟内存管理的一个原因吧。这只是个人观点,求拍砖.....)
当P置0时intel给出了段描述符的格式如下:
linux中的分段 - yangfan876@126 - yangfan876@126的博客
 
DPL:段的特权级,范围从0~3,但是linux中只用了0和3这两个特权级。

S:如果该位被置0则表示该段为系统段,里面存储了LDT等这种关键的数据结果。如果为1则表示该段为一普通的代码段或数据段等。

TYPE:描述了段的类型特征和它的存取权限。
如下,intel给出了TYPE成员的不同值所对应的权限表:
linux中的分段 - yangfan876@126 - yangfan876@126的博客
 

那么如何通过段机制将逻辑地址转换成线性地址,下面将进行说明:

intel给出了三种使用分段的模式,分别是基本平模式,保护平模式,和多段模式,并且给出了三种分段模式的示意图如下:
linux中的分段 - yangfan876@126 - yangfan876@126的博客
如上便是基本平模式,定义段机制为0,最大偏移量为4G(FFFFFFFFH),将所有的段机制寄存器指向同一个段描述符。
 
linux中的分段 - yangfan876@126 - yangfan876@126的博客
 此图为保护平模式,他是将代码段和其他段分开存放。
linux中的分段 - yangfan876@126 - yangfan876@126的博客
 此图位多段模式,将所有的段都分开。

首先声明,在linux中使用了第一种最简单的分段模式,至于为什么使用这中分段模式,ULK中给出了解释,大致是因为linux需要移植到大多数平台上,但是RISC的体系对分段支持有限;同时,当所有的进程都使用相同的段寄存器的值的时候内存管理将变得简单。

以上便是linux段机制的简单总结.....

这篇关于Linux分段的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1042363

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_wo