本实验参考笔记 love6.blog.csdn.net/article/det…
因为我想配置云端实验环境,尝试了azure VM创建ubuntu 虚拟机,但是当时我不懂配置gnome图形界面,所以在启动bochs 的时候看不到GUI,差点一度放弃;然后尝试azure vm winserver,在winserver内用hyper-v创建ubuntu 但是没有网络,配置了几天没搞懂怎么设置;最后尝试vncserver 获取ubuntuserver的图形化页面信息,才得以进行下去。期间曾经考虑是否要换到THU的rcore项目。
原理
花较短篇幅解释为什么要有这个实验环境
bochs安装和配置
基本参考 进行安装
1 下载tar.gz
curl -L -O https://github.com/bochs-emu/Bochs/archive/refs/tags/REL_2_8_FINAL.tar.gz
因为教程依赖的是2.6.2 这个tag 所以先git clone 原始仓库,然后再进行checkout 到对应的tag
注意要cd 到 bochs 文件夹执行下面命令
./configure \
--prefix=/your_path/bochs \
--enable-debugger \
--enable-disasm \
--enable-iodebug \
--enable-x86-debugger \
--with-x \
--with-x11
然后就是 sudo make && sudo make install
但是,如果你的ubuntu 是新的,会报错如下,有其他工具需要安装,这些问题遇到的时候问gpt
执行下面的
sudo apt update
sudo apt install libx11-dev libxext-dev libxmu-dev libxrandr-dev -y
还是报错
ERROR: pkg-config was not found, or unable to access the gtk+-2.0 package.
Install pkg-config and the gtk+ development package,
or disable the gui debugger, or the wxWidgets display library (whichever is being used).
执行下面
sudo apt update
sudo apt install pkg-config libgtk2.0-dev -y
再次运行 sudo make && sudo make install 成功,之后都要切换到一个名为cooil...的账户下实验
接下来还有bochs 配置文件需要修改
找到安装路径下的bochs 配置文件 /home/cooiboi/bochs
编辑这个配置文件 保存到 bochs 的启动文件bochs bin 所在的bin/ 文件夹下
需要创建硬盘否则启动bochs 失败, 使用bin/bximage 创建 执行 sudo ./bximage 他会在当前目录下创 hd ,
dd if=/home/lenny/git/assemble/mbr.bin of=/home/lenny/git/assemble/bochs-2.6.8/hd60M.img bs=512 count=1 conv=notrunc
结果输出如下图
我把bochsrc.disk 和bochs 存储在同一个文件夹,因此在启动bochs 的时候指定boch config file
问题1
可以看到,问题主要是CPU的 malform, 查询了bochsrc.disk
以下部分可能是问题的根源:
cpu: model=core2_penryn_t9600, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
问题分析
model=core2_penryn_t9600
不被支持: Bochs 的model
参数仅支持有限的 CPU 类型名称,比如core2
或pentium
等,而core2_penryn_t9600
是无效的值。- 配置项符合性问题: Bochs 的配置文件要求严格,任何额外的未定义参数都会导致解析错误。
解决方案
-
修正
model
值:替换为支持的 CPU 模型(例如core2
或pentium
)。修改如下:cpu: model=core2, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1, msrs="msrs.def"
-
验证是否需要
msrs
参数:如果你不确定msrs="msrs.def"
是否必要,可以暂时去掉此参数,简化配置,测试是否正常运行:cpu: model=core2, count=1, ips=50000000, reset_on_triple_fault=1, ignore_bad_msrs=1
-
确认
ips
参数值:ips
是指每秒执行的指令数。如果出现性能或兼容性问题,可以尝试减少到较小的值,例如:ips=1000000
-
测试配置:保存修改后的配置文件,重新运行 Bochs:成功
./bochs -f ./bochsrc.disk
问题2
似乎是keyboard mapping 的配置无法理解 dam 大佬骗我 修改一下keyboard 就好
问题3
需要关闭声卡
禁止使用之后的,再次执行终于没有额外问题 直接到6
但是点击6 开始模拟 出现问题 如下
bochsrc.disk:85: display library 'nogui' not available
切换使用vnc viewer 终于正常显示
但是我的simulator 是一片黑,什么都没有,在大佬的笔记中看到这句话,确实我复现了,
在terminal 输入c ,simulator 有显示,如下图
根据这个图,报错消息和大佬一样,no bootable device, 原因就是我们输入的CHS信息,这个硬盘空 间内没有合适的MBR程序
所以我们要把MBR binary file 拷贝到硬盘空间内
编写好mbr.S 并使用sudo nasm 编译成bin文件
使用这个命令考吧到硬盘:
dd if=./mbr.bin of=./hd60M.img bs=512 count=1 conv=notrunc
使用 dd
命令来操作 硬盘镜像文件 (hd60M.img
) ,它的作用是将 mbr.bin
文件写入 hd60M.img
的前 512 字节,通常用于 写入主引导记录(MBR, Master Boot Record) 。下面是具体解释:
命令参数解析
参数 | 含义 |
---|---|
dd | 数据复制命令,用于低级别复制和转换数据。 |
if=./mbr.bin | 输入文件 (if = input file),这里是 mbr.bin ,通常是一个 512 字节的主引导记录(MBR)二进制文件。 |
of=./hd60M.img | 输出文件 (of = output file),这里是 hd60M.img ,即 60MB 的虚拟磁盘映像文件。 |
bs=512 | 设定块大小为 512 字节(与 MBR 大小一致)。 |
count=1 | 只复制 1 块(即 512 字节),刚好覆盖 MBR 区域。 |
conv=notrunc | 不截断输出文件,即保持 hd60M.img 的原始大小,仅覆盖前 512 字节。 |
输出如下图
再次启动bochs, 成功如下图
第一章算是成功完结
后续是本章我在阅读时候的一些笔记
. 驱动程序的作用
- 什么是驱动程序:
-
- 驱动程序(Driver)是操作系统和硬件设备之间的桥梁。
- 它是一段软件,允许操作系统与硬件设备进行通信,使得应用程序能够通过操作系统间接控制硬件。
- 驱动程序的作用:
-
-
抽象硬件功能:不同的硬件有各自的物理特性和通信协议,驱动程序将这些硬件特性抽象为操作系统可以理解的接口。
-
提供统一接口:驱动程序隐藏了硬件细节,为操作系统提供一致的操作接口。
-
实现硬件的可用性:如果没有驱动程序,操作系统和软件无法直接使用硬件功能。
-
用户态和内核态区别
为什么内核态被称为管态?
- 完全控制硬件资源:
-
- 内核态可以直接操作CPU的寄存器、访问物理内存和外设。
- 内核态允许执行诸如设置中断向量表、切换进程上下文等高级操作。
- 负责系统管理:
-
- 内核态是操作系统的核心运行环境,负责整个系统的监督与管理,包括:
-
- 进程调度
- 内存管理
- 文件系统
- 中断处理
- 硬件控制
- “管态”的历史原因:
-
- 在早期计算机(如 IBM 大型机)的设计中,Supervisor Mode(监督模式) 是一种专门为操作系统设计的特权状态。
- 随着计算机体系结构的演进,这一概念被保留并逐渐演变为现代的“内核态”(Kernel Mode),在中文中也称为“管态”或“核心态”。
- 安全性和隔离性:
-
- 管态(内核态)与用户态的划分是为确保系统的安全性和稳定性。
- 用户程序无法直接进入管态,必须通过合法的入口(如系统调用)进入,这种机制保证了操作系统的核心不会被恶意程序破坏。
3. 管态的特权功能
在管态下,CPU可以执行以下特权操作:
- 直接访问硬件设备:
-
- 控制外设,如磁盘、网络设备、显示器等。
- 管理内存:
-
- 设置页表、切换地址空间。
- 控制进程调度:
-
- 切换进程上下文,分配CPU时间片。
- 处理中断:
-
- 接收硬件中断信号并调用对应的中断服务程序
用户态程序的内存访问受限
-
不能直接访问物理内存:
- 物理内存的分配是由内核(管态)控制的,用户态程序无法直接操作物理地址空间。
- 现代操作系统使用 虚拟内存(Virtual Memory) 机制,用户态程序只能访问操作系统为其分配的虚拟地址空间,而真实的物理内存管理是内核的职责。
-
用户态程序运行在受限模式下:
-
用户态程序没有权限访问或修改 MMU(内存管理单元) 的配置,例如不能直接修改 页表(Page Table) 。
-
任何试图访问未被操作系统分配的内存地址(例如越界访问)都会触发 访问异常(Segmentation Fault) 。
-
内存分段和平坦模型
以下是它们的联系与区别:
1. 什么是内存分段(Memory Segmentation)?
- 定义:
-
- 内存分段是一种将内存划分为若干段(Segment)的机制,每个段代表一块逻辑区域,例如代码段、数据段、堆栈段等。
- 每个段都有自己的基地址和大小限制,程序通过段选择器(Segment Selector)和段内偏移量(Offset)来访问内存地址。
- 工作机制:
-
- CPU 使用段寄存器(如 CS、DS、SS 等)保存各段的基地址。
- 内存地址通过段基地址 + 偏移量计算得出物理地址。
- 分段模型强调逻辑地址到物理地址的映射。
- 优点:
-
- 支持模块化程序设计,将代码、数据、堆栈等区域逻辑分离。
- 方便内存的保护和权限控制,可以为不同段设置不同的权限(只读、可写等)。
- 缺点:
-
- 段的划分和管理比较复杂,容易导致内存碎片。
- 如果程序跨段访问,可能增加地址计算的复杂性。
2. 什么是内存平坦模型(Flat Memory Model)?
- 定义:
-
- 内存平坦模型将整个内存视为一个线性连续的地址空间,不区分逻辑段。
- 程序通过直接访问线性地址(Logical Address)与物理地址进行操作。
- 工作机制:
-
- 平坦模型中,内存地址直接通过一个线性地址空间表示,不涉及段寄存器或段偏移的概念。
- 所有的内存访问都是基于一个统一的地址空间进行。
- 优点:
-
- 地址计算简单,方便程序开发和调试。
- 消除了内存碎片问题,因为内存是统一连续的。
- 缺点:
-
- 内存的保护和隔离机制较弱(需要其他技术如分页来配合实现)。
- 不支持像分段模型那样的灵活分区管理。
3. 联系
- 都是内存管理的一种方式:
-
- 分段和平坦模型都是为了让程序更方便地访问内存,组织和管理内存地址。
- 它们的目标都是高效利用内存资源。
- 可以结合使用:
-
- 在现代计算机中,分段和分页(平坦模型的一部分)常结合使用。
- 例如:x86 架构允许同时启用分段和分页功能,其中分页提供平坦的线性地址空间,而分段用于逻辑地址的划分。
4. 区别
特性 | 内存分段 | 内存平坦模型 |
---|---|---|
内存组织方式 | 将内存划分为若干逻辑段,每个段有独立的基地址和大小 | 将内存视为一个连续的线性地址空间 |
地址计算 | 通过段基地址 + 偏移量计算物理地址 | 地址直接对应内存的线性地址 |
复杂性 | 复杂,需管理段寄存器和偏移量 | 简单,所有地址直接使用线性地址访问 |
内存保护 | 每个段可以设置不同的访问权限(只读、可写等) | 本身没有保护机制(需要配合分页等技术) |
程序设计 | 程序需明确管理段的切换和访问 | 程序设计更加直观,适合现代高层语言 |
主要应用场景 | 早期操作系统(如 DOS)、嵌入式系统、x86保护模式 | 现代操作系统(如 Linux、Windows 平坦模式) |
5. 实例说明
(1) 内存分段模型
在 x86 保护模式下:
- 内存被分为代码段(CS)、数据段(DS)、堆栈段(SS)等。
- 程序通过
CS:IP
(段地址 + 偏移量)访问代码,通过DS:SI
访问数据。
(2) 内存平坦模型
在现代操作系统中(如 Linux):
- 整个内存被看作 0 到最大物理地址的一个线性区域。所有进程的地址空间都是从 0 开始的线性地址,依靠 分页(Paging) 进行地址映射。
- 程序通过虚拟地址访问内存,内存的分段和权限管理由分页机制和内核完成。
16位寄存器表示20位内存地址
方法1 CPU自己解决
段地址只保存物理地址的高 16 位,而后 4 位通过偏移量的低 4 位补充计算出来,从而形成完整的 20 位物理地址。这种机制是 8086 CPU 的内存分段模型的核心思想。
段地址和偏移量的组合机制
物理地址的计算公式为:
具体解析:
- 段地址的作用:
-
- 段地址由段寄存器(如
CS
、DS
、SS
)保存,它表示物理地址的高 16 位部分。 - 因为段地址以 16 字节为单位,所以段地址的每一位实际代表物理地址的 4 个 bit(左移 4 位)。
- 段地址由段寄存器(如
- 偏移量的作用:
-
- 偏移量由偏移寄存器(如
BX
、SI
、DI
)或立即数指定,它用于补充物理地址的低 16 位部分。 - 偏移量可以动态变化,从而灵活地访问段内的不同地址。
- 偏移量由偏移寄存器(如
- 物理地址的组合:
-
- 将段地址左移 4 位(乘以 16),作为物理地址的高位部分。
- 偏移量直接加到段地址的结果上,补充物理地址的低位部分。
示例分析
示例 1:段地址与偏移量的简单组合
假设段地址为 0x2000
,偏移量为 0x0034
:
- 段地址: 表示物理地址的高 16 位。
- 偏移量: 表示物理地址的低位补充。
- 物理地址:
示例 2:跨段访问
如果偏移量超过 64KB 的范围,需要切换段地址:
- 假设当前段地址为
0x3000
,偏移量为0xFFFF
: - 如果偏移量再增加 1,就需要调整段地址:
-
- 新的段地址可能是
0x4000
,偏移量重新从0x0000
开始。
- 新的段地址可能是
方法2 使用编码方式解决
这种方法的核心是利用段地址和偏移量的组合,并通过立即数直接作为偏移量,避免偏移量必须从寄存器中读取
保护模式 和 内核模式,它们分别属于 CPU的运行模式 和 操作系统的运行特权 的概念。以下是详细解答:
1. CPU什么时候会进入保护模式?
CPU进入保护模式通常发生在 系统启动过程中,以下是具体情况:
- 启动流程概述:
-
- CPU加电启动时,默认运行在 实模式(Real Mode)。这种模式是为了兼容早期的x86架构,允许访问1MB内存。
- 操作系统的引导程序(Bootloader)负责完成从实模式到保护模式的切换。
- 进入保护模式的步骤:
-
- 设置控制寄存器(CR0) :
-
- 向控制寄存器
CR0
的第 0 位(保护模式启用位,PE)写入1
,启用保护模式。
- 向控制寄存器
- 加载全局描述符表(GDT) :
-
- 在进入保护模式之前,操作系统会初始化 GDT(全局描述符表),为内存分段和权限管理提供基础。
- 切换代码段寄存器(CS) :
-
- 切换
CS
(代码段寄存器)以指向保护模式下的有效段描述符。
- 切换
- 启用分页(可选) :
-
- 保护模式支持分页机制(通过 CR3 寄存器设置页表),这一步通常由操作系统完成。
- 何时进入保护模式?
-
- 现代计算机在系统启动时:
-
- 在操作系统加载之前,Bootloader(如GRUB)会将 CPU 切换到保护模式。
- 从实模式切换到保护模式:
-
- 当系统需要超过 1MB 的内存访问或需要多任务和虚拟内存功能时。
2. 保护模式与内核模式的区别
这两个术语涉及不同的层面:
特性 | 保护模式 | 内核模式 |
---|---|---|
概念层面 | CPU 的硬件运行模式 | 操作系统的运行权限级别 |
目标 | 管理内存访问、支持多任务和虚拟内存 | 提供对硬件资源和敏感操作的访问控制 |
运行范围 | CPU 启动保护模式后,所有进程运行在保护模式下 | 操作系统的内核运行在内核模式,应用运行在用户模式 |
硬件支持 | 使用段寄存器(如 CS、DS)和 GDT/TSS 等硬件机制 | 通过 CPU 提供的特权级别(Rings)区分权限 |
切换方式 | 通过设置 CR0 的 PE 位进入保护模式 | 通过系统调用(如 syscall 或 int 指令)进入内核模式 |
保护模式下的持续运行
一旦 CPU 进入 保护模式(Protected Mode) 后,通常操作系统会继续运行在这个模式下。这是因为保护模式相比于实模式有以下显著优势:
- 支持更大的内存空间:保护模式允许访问 4GB 或更多内存(通过扩展的地址总线)。
- 多任务支持:保护模式支持硬件级别的多任务机制,可以运行多个程序并通过操作系统管理资源。
- 安全性:保护模式引入了分段和分页机制,为程序提供内存保护,防止程序之间互相干扰。
- 增强的指令集:保护模式支持更多的 x86 指令集。
因此,现代操作系统(如 Windows、Linux)会一直运行在保护模式下,除非有特殊需求。
程序分段
,这三个段(代码段、数据段、BSS段)在现代计算机和操作系统中仍然广泛使用,尤其是在编译和链接过程以及可执行文件(如 ELF 文件)的内存布局中。这些段的用途如下:
1. 代码段(Code Segment)
- 现状:依然被使用。
- 功能:存储程序的可执行指令(只读)。
- 特点:
-
- 通常标记为只读和可执行,以保护代码不被修改。
- 在现代安全机制下,操作系统通过启用**不可执行内存(NX bit)**确保只有代码段可以被执行,防止代码注入攻击。
2. 数据段(Data Segment)
- 现状:仍然使用。
- 功能:存储已初始化的全局变量和静态变量。
- 特点:
-
- 数据段的内容在程序启动时就加载到内存中。
- 它通常可读写,用于程序执行期间需要的全局变量。
3. BSS段(BSS Segment)
- 现状:依然存在。
- 功能:存储未初始化的全局变量和静态变量。
- 特点:
-
- BSS 段在内存中被初始化为 0,节省了存储空间,因为它不会占用磁盘文件空间,只在运行时分配。
- 现代编译器和链接器仍然会将未初始化的变量分配到 BSS 段。
**
**
解读汇编
下面是对上述汇编代码的理解
jmp 0x90:start
的作用
- 目的:
-
- 这条指令是一个远跳转(far jump) ,直接修改了段寄存器
CS
和指令指针IP
的值。 - 将程序的执行流跳转到代码段的入口地址,并同时初始化
CS
寄存器为0x90
。
- 这条指令是一个远跳转(far jump) ,直接修改了段寄存器
- 背景:
-
- 为什么是
0x90
? -
-
程序在加载到内存时,被 MBR 放在物理地址
0x900
(即内存的某个区域)。 -
在实模式下,段寄存器
CS
的值是段基址,指向实际物理地址的段基址 × 16
。因此:
0x90 × 16 = 0x900
。 -
这条指令将 CPU 的代码段寄存器设置为
0x90
,使得指令可以正确地从0x900
处开始执行。
-
- 为什么是
解释DS 数据段基质
mov ax, section.my_data.start
add ax, 0x900
这两行代码的意思,把数据段的段起始地址加载到ax这个寄存器,同时这个寄存器是16位,然后把0x900 这个代表整个代码起始地址添加到ax这个寄存器中,所以,在ax 中存储的是数据段的物理地址;
-
如果使用
add ax, 0x90
,那么计算结果将是错误的,因为0x90
是段基址,而不是物理地址。 -
段基址本身并不能直接参与内存地址的计算,它需要通过
段基址 × 16
转换为物理地址。而0x900
已经是物理地址。
shr ax, 4
mov ds, ax
是为了满足段寄存器(如 DS 数据段寄存器)的地址格式要求,因为在上述计算中,已经在ax 得到数据段开始的物理地址,需要右移4位,获取段地址,存储到ds
解释栈基址
先分析下面的代码
section my_stack align=16 vstart=0
times 128 db 0
stack_top:
(1)section my_stack align=16 vstart=0
section my_stack
:-
- 定义了一个名为
my_stack
的栈段。 - 该段被分配了一块独立的内存区域,用于存储栈数据。
- 定义了一个名为
align=16
:-
- 指定该段的起始地址是 16 字节对齐的,符合段寄存器对地址对齐的要求。
vstart=0
:-
- 指定该段的逻辑起始地址为 0。也就是说,该段内的第一个字节的偏移量为
0
。
- 指定该段的逻辑起始地址为 0。也就是说,该段内的第一个字节的偏移量为
(2)times 128 db 0
- 这行指令分配了 128 字节的内存空间,并用
0
初始化所有字节。 - 栈段内存的布局如下:
-
- 从偏移量
0x0
到0x7F
是这段分配的内存空间。
- 从偏移量
(3)stack_top:
stack_top
是一个标号,指向当前段(my_stack
)的末尾。- 标号的位置是内存的偏移量
0x80
(十六进制),也就是这段内存的最高地址;栈的最高有效地址是栈空间的最后一个字节的位置,也就是偏移量0x7F;但是,现在栈内没有其他的元素,所以栈顶只能是0x80
**
**
之后,下面的stack segment init 就很好理解
**
**
mov ax, section.my_stack.start ; 加载栈段的偏移地址
add ax, 0x900 ; 加载实际物理地址
shr ax, 4 ; 计算段基址
mov ss, ax ; 初始化栈段寄存器 SS
mov sp, stack_top ; 初始化栈指针 SP,设置栈顶位置
**
**
其中,栈顶位置stack_top 就是之前的0x80
解释自定义数据段内容
这段代码定义了一个名为 my_data
的数据段,并在其中声明了两个全局变量 var1
和 var2
。下面逐行解析其含义:
1. section my_data align=16 vstart=0
section my_data
:-
- 定义一个新的数据段,段名为
my_data
。 - 这是一个逻辑段,用于存储程序中的全局变量和常量。
- 定义一个新的数据段,段名为
align=16
:-
- 指定该段的起始地址是 16 字节对齐的。这是为了满足 x86 实模式下段地址的对齐要求。
vstart=0
:-
- 表示该段的逻辑起始地址为
0
。 - 即段内变量的偏移地址从
0x0
开始计算。
- 表示该段的逻辑起始地址为
2. var1 dd 0x1
var1
:-
- 声明了一个名为
var1
的变量。 dd
(define double word) :-
- 定义一个占用 4 字节(32 位)的变量。
- 对应汇编语言中常用的数据类型,适用于存储 32 位整数或指针。
0x1
:-
- 初始化
var1
的值为0x1
。
- 初始化
- 声明了一个名为
- 存储细节:
-
-
假设段起始偏移地址为
0x0
,则var1
的偏移地址为:var1 的偏移地址 = vstart + 0x0 = 0x0
-
它占用 4 字节的内存,从
0x0
到0x3
。
-
3. var2 dd 0x6
var2
:-
- 声明了一个名为
var2
的变量。 - 同样使用
dd
定义,占用 4 字节。
- 声明了一个名为
- 存储细节:
-
-
var2
的偏移地址紧跟在var1
后面,从0x4
开始。var2 的偏移地址 = vstart + 0x4 = 0x4
-
它占用 4 字节的内存,从
0x4
到0x7
。
-
4. 段的布局
这段代码定义的 my_data
段的布局如下:
偏移地址 | 内容 | 描述 |
---|---|---|
0x0 | 0x00000001 | var1 的值 |
0x4 | 0x00000006 | var2 的值 |
整个数据段占用 8 字节的内存。
5. 使用示例
在程序中可以通过数据段寄存器(DS
)和偏移地址访问这些变量:
读取 var1
的值
mov ax, ds:[0x0] ; 从 DS 段偏移地址 0x0 读取 var1 的值
读取 var2
的值
mov ax, ds:[0x4] ; 从 DS 段偏移地址 0x4 读取 var2 的值
**
**
在 x86 汇编中,数据宽度的单位有以下常见类型:
-
byte
:8 位(1 字节)。 -
word
:16 位(2 字节)。 -
dword
(double word):32 位(4 字节)。 -
qword
(quad word):64 位(8 字节)
push word [var2]
的含义
push
指令:-
- 将数据压入栈。
- 栈的增长方向是从高地址向低地址,因此执行
push
时,栈指针(SP
)会减少。 - 数据会写入栈段(由
SS:SP
确定的位置)。
word
的作用:-
- 明确表示压栈的数据宽度是 16 位(2 字节) 。
- 指令会从
var2
的地址中读取 16 位(2 字节) 数据,并压入栈中。
[var2]
:-
-
表示从变量
var2
的地址开始读取数据。 -
var2
是一个dword
(双字,4 字节)的变量,但这里通过word
修饰符,指示只读取变量的 低 16 位。
-
-
在保护模式下,段地址 + 偏移地址 得到的并不是物理地址,而是线性地址。
-
如果启用了分页机制,线性地址会通过页表的转换,最终映射到物理地址。
-
分页机制的目的是支持更大的地址空间和内存虚拟化,因此它是现代操作系统(如 Linux 和 Windows)的核心机制。
实模式和保护模式的地址计算区别
在实模式下:
- 段基址 × 16 + 偏移地址直接得出物理地址,这个地址可以直接用于访问内存。
- 实模式设计的初衷是向后兼容简单的16位环境(如 MS-DOS)以及早期的 x86 架构,这些环境下不存在虚拟地址的概念。
虚拟地址是为了支持操作系统的内存管理而引入的。实模式运行在 CPU 刚刚上电的初期,系统还没有进入保护模式,也没有复杂的操作系统参与,因此不具备虚拟地址的条件。
. 实模式下的地址计算
-
在实模式下,内存地址由 段基址 和 段内偏移 通过以下公式直接计算得出:
Copy code 物理地址 = 段基址 × 16 + 段内偏移
-
实模式下的地址计算非常简单,直接输出一个 物理地址,可以用来访问内存。
-
如图所示:
-
-
段基址为
0xC0
,偏移地址为0x01
,计算出的物理地址为:Copy code 物理地址 = 0xC0 × 16 + 0x01 = 0xC01
-
-
结论:
-
- 在实模式下,段地址 + 偏移地址直接得到物理地址,无需分页或虚拟地址的转换。
2. 保护模式下的地址计算
- 保护模式引入了分段机制和分页机制:
-
- 分段机制:
-
- 段地址由选择子索引到 GDT(全局描述符表),通过段描述符计算出段基址。
- 段基址 + 偏移地址得到 线性地址。
- 分页机制(可选):
-
- 如果分页功能开启,线性地址需要通过页表转换为物理地址。
- 如图所示,如果分页功能已开启,则线性地址被认为是虚拟地址,需要经过页表转换为物理地址。
- 结论:
-
- 保护模式下是否使用分页取决于是否启用了分页功能(由 CR0 寄存器的第 31 位控制)。
为什么在保护模式中,存储在寄存器中的段基址 会变成选择子selector ?
段选择子(Selector) 的确相当于全局描述符表(GDT)或本地描述符表(LDT)中的一个索引,通过它,CPU 可以快速定位段描述符
举一个例子, 在real mode 中完成GDT如下图
GDT 的定义和初始化可以在 Real Mode(实模式)完成,但它的使用是在保护模式(Protected Mode)中生效的
-
软中断:软中断是通过软件触发的一种中断机制,通常由程序发起。比如 Linux 使用
int 0x80
指令(x86 架构)进入内核,从用户态切换到内核态,处理系统调用请求。 -
硬中断:硬中断是由硬件触发的中断机制,例如键盘输入、鼠标点击或硬盘读写请求等。这些中断是由硬件直接发出信号给 CPU。
但是 栈支持LIFO
编译器的自举过程
(1)新功能的引入(支持转义符 \
)
- 起点:
-
- 老编译器
compile_old.c
不能处理\
这种转义符。 - 为了支持这一功能,开发者修改了编译器的源代码,生成了
compile_new_a.c
。
- 老编译器
- 问题:
-
- 老编译器虽然可以编译
compile_new_a.c
,生成新的编译器compile_new_a
,但它无法正确处理支持\
的新特性。 - 新功能需要进一步的编译器更新。
- 老编译器虽然可以编译
- 结果:
-
- 用老编译器编译
compile_new_a.c
,生成compile_new_a
,它可以支持\
。
- 用老编译器编译
(2)迭代改进(支持换行符 \n
)
- 在支持
\
的基础上,再扩展支持\n
。 - 修改
compile_new_a.c
,生成新的源码compile_new_b.c
,然后用支持\
的编译器(compile_new_a
)去编译compile_new_b.c
。 - 问题:
-
compile_new_a
可以识别\
,但无法直接识别\n
,需要使用其 ASCII 值(10)作为中间表示。- 通过这种方式,逐步完成对
\n
的支持。
- 结果:
-
- 新的编译器版本
compile_new_c
生成后,可以直接识别\n
,功能更加强大。
- 新的编译器版本
编译型和解释性代码的区别
JS 脚本程序:
-
- 运行时会通过 Node.js 引擎 或 浏览器内核(如 V8 引擎)来解释执行。
- CPU 看到的实际上是 Node.js 或 浏览器进程。
- 这些引擎会负责读取 JS 源代码,解释为机器码供 CPU 执行。
-
编译型程序:
-
如果你用 C++ 写一个程序并编译成可执行文件,CPU 可以直接看到编译后的程序进程。
-
操作系统调度器直接调度的是编译生成的二进制程序。
-
查询ubuntu 系统,发现居然是小端序列
假设有一个 16 位的数据 0x1234
,存储到内存中时的字节排列:
(1)大端序存储方式:
- 内存地址从低到高排列:第一个字节是
0x12
,第二个字节是0x34
。 - 存储顺序:
-
- 内存地址 0x00:
0x12
- 内存地址 0x01:
0x34
- 内存地址 0x00:
(2)小端序存储方式:
- 内存地址从低到高排列:第一个字节是
0x34
,第二个字节是0x12
。 - 存储顺序:
-
-
内存地址 0x00:
0x34
-
内存地址 0x01:
0x12
-
大端序的优点:
- 更符合人类阅读习惯,因为字节顺序和书写顺序一致。
- 适合网络传输协议(例如 TCP/IP),因此很多网络协议使用大端序。 这个是重点
小端序的优点:
-
在处理低位对齐时效率更高,例如从内存中读取 16 位或 32 位整数时,不需要调整地址。
-
现代大多数计算机(如 x86)都采用小端序。
TCP IP 数据在进行网络传输,我觉得问题就出现在接收数据的时候没有转换成主机序列
-
发送数据时:
- 将主机字节序(Host Byte Order)转换为网络字节序(Network Byte Order)。
- 例如,将一个 32 位整数从小端序转换为大端序。
-
接收数据时:
- 将网络字节序转换为主机字节序,方便主机解析。
如果传输的是字符串
**如果你发送的数据长度是 100 字节,你是否需要使用 htonl
或 ntohl
进行字节序转换,取决于数据的类型和内容。**以下是具体分析和推荐:
1. 发送的数据类型
- 整数数据:
-
- 如果你的 100 字节数据中包含整数类型的数据(如
int32_t
、int16_t
),那么这些整数需要进行字节序转换。 - 例如:
-
- 使用
htonl
转换 32 位整数(如uint32_t
)。 - 使用
htons
转换 16 位整数(如uint16_t
)。
- 使用
- 如果你的 100 字节数据中包含整数类型的数据(如
- 字符串或纯字节数组:
-
- 如果数据是字符串(如
"Hello, World!"
)或者是任意的字节流,则不需要进行字节序转换。 - 原因是字符串和字节流的每个字节在网络中传输时是独立的,不会受到字节序影响。
- 如果数据是字符串(如
2. 发送 100 字节的数据
情景 1:如果数据是整数数组
例如,数据是由 25 个 32 位整数组成(每个占 4 字节,共 100 字节):
uint32_t data[25];
在发送之前,需要将每个整数从主机字节序转换为网络字节序:
for (int i = 0; i < 25; i++) {
data[i] = htonl(data[i]); // 转换每个整数
}
send(sockfd, data, sizeof(data), 0);
情景 2:如果数据是字符串或字节流
如果数据是字符串(如 "Hello, World!"
)或者未解释的字节流:
char data[100] = "Some binary or text data...";
这种情况下,不需要使用 htonl
或 ntohl
,可以直接发送:
send(sockfd, data, sizeof(data), 0);
如果发送的数据是字符串或纯字节流,发送时不需要转换成网络字节序。在接收端,也不需要进行字节序转换,可以直接使用接收到的数据。
3. 如何判断是否需要字节序转换
- 需要转换的情况:
-
- 数据中包含需要解析的多字节整数(如 IP 地址、端口号、文件偏移量等)。
- 接收方需要以正确的字节序解析这些数据。
- 不需要转换的情况:
-
- 数据是字符串、单字节字符数组,或者未解释的二进制数据(字节流)。