操作系统---在内核中重新加载GDT和堆栈
摘要
用BIOS方式启动计算机后,BIOS先读取引导扇区,引导扇区再从外部存储设备中读取加载器,加载器读取内核。进入内核后,把加载器中建立的GDT复制到内核中。
这篇文章的最大价值也许在末尾,对C语言指针的新理解。
是什么
在BOOT(引导扇区)加载LOADER(加载器)。
在LOADER中初始化GDT、堆栈,把Knernel(内核)读取到内存,然后开启保护模式,最后进入Knernel并开始执行。操作系统正式开始运行了。
GDT是CPU在保护模式下内存寻址必定会使用的元素,在Kernel执行过程中也需要用到。
在内核中重新加载GDT和堆栈,是指,把存储于LOADER所使用的内存中的GDT数据和堆栈中的数据复制到Kernel所使用的内存中。关键点不是Kernel和LOADER所使用的内存,而是变量。换句话说,把存储在LOADER中的变量中的GDT数据和堆栈中的数据复制到Kernel变量中的GDT和堆栈。
LOADER是用汇编写的,“汇编中的变量”,不知道这种表述是否准确。
为什么
理由很简单。LOADER是用汇编语言写的,Kernel主要用C语言开发。在Kernel中使用GDT,若使用LOADER中定义的那个GDT变量(或者叫标号),光想一想就觉得很混乱。
用一句解释:C语言中使用C语言中的变量更方便。
怎么做
流程
- 在kernel中声明变量
unsigned short gdt_ptr
,存储GDT的内存地址。 - 使用
sgdt
指令把GDT的内地址复制到gdt_ptr
中。 - 在kernel中创建结构体
gdt
,存储GDT。 - 使用内存复制函数把GDT从LOADER中设置的内存位置复制到kernel中的变量
gdt
表示的内存中。
memcpy
它是内存复制函数。
这样实现它:
- 原型是:
memcpy(void *dst, void *src, int size)
。 - 核心是,把数据从
[ds:esi]
移动到[es:edi]
。 - 以字节为单位来复制数据,复制
size
次。 - 用
jmp
实现循环,不用loop
。 - 循环终止条件是:size = 0。
memcpy:
push ebp
mov ebp, esp
push edi
push esi
push ecx
push eax
push ds
push es
mov es, [ebp + 12] ;dst
mov ds, [ebp + 8] ; src
mov size, [ebp + 4] ; size
mov edi, 0
mov esi, 0
mov ecx, size
.1:
cmp ecx, 0
jz .2
mov al, [ds:esi]
mov [es:edi], al
inc esi
inc edi
dec ecx
.2:
pop es
pop ds
pop eax
pop ecx
pop esi
pop edi
pop ebp
ret
gdt
typedef struct {
unsigned short limitLow;
unsigned short baseAddressLow;
unsigned char baseAddressMid;
unsigned char attribute1;
unsigned char attribute_limit;
unsigned char baseAddressHigh;
}Descriptor;
Descriptor gdt[128];
堆栈
[SECTION .bss]
StackSpace resb 2 * 1024
StackTop:
mov esp, StackTop
不理解。
代码
C语言
// 声明一个char数组,存储GDT的内存地址
unsigned char gdt_ptr[6];
nasm汇编
; 使用C语言中声明的变量gdt_ptr
extern gdt_ptr
; 把寄存器gdtr中的数据复制到变量gdt_ptr中
sgdt [gdt_ptr]
然后在C语言中把LOADER中的GDT复制到C语言中的gdt变量中。
memcpy(&gdt,
(void *)((*)(int *)(&gdt_ptr[2])),
(*)((int *)(&gdt_ptr[0]))
);
short *gdt_limit = &gdt_ptr[0];
int *gdt_base = &gdt_ptr[2];
*gdt_limit = 128 * sizeof(Descriptor) - 1;
*gdt_base = (int) &gdt;
难点解读
memcpy的参数
上面的那段代码,理解起来难度不小。
memcpy(&gdt,
(void *)((*)(int *)(&gdt_ptr[2])),
(*)((short *)(&gdt_ptr[0]))+1
);
memcpy
的第一个参数是目标内存地址,是一个指针类型变量,赋值应该是一个内存地址,所以用&
取得变量gdt的内存地址。
- 理解
(void *)((*)(int *)(&gdt_ptr[2]))
:- 第二个参数是源数据的内存地址,是GDT的物理地址。
- 它存储在
gdt_ptr
的后6个字节中。 &gdt_ptr[2]
获取gdt_ptr的第3个元素gdt_ptr[2]
的物理地址。- 前面的
(int *)
将这段物理地址强制类型转换为一个指针,这个指针的数据类型是int *
。 - 数据类型是
int *
有三层含义:- 这个数据是一个指针。
- 这个数据的值是一个内存地址。
- 这个内存地址是一个4字节(int类型占用4字节)内存区域的初始地址。
&gdt_ptr[2]
是一个内存地址,用(int *)
将它包装成或强制转换成指针类型。- 再用
*
运算符,是获取这个内存地址指向的内存区域中的数据。 - 这个数据是
int
类型,占用4个字节。这4个字节的初始地址是&gdt_ptr[2]
。这是最关键的一句。 - 为什么最后还要用
void *
?- 因为,这4个字节中存储的那个
int
数据又是一个内存地址,因此,需要再次包装成一个指针。 - 因为,
memcpy
对参数的数据类型要求是void *
。 - 究竟是哪个原因,我也不知道。
- 因为,这4个字节中存储的那个
- 理解:
(*)((short *)(&gdt_ptr[0]))+1
- 为什么要加1?
gdt_ptr
的低2位保存的是GDT的字节偏移量的最大值,是GDT的长度减1。 &gdt_ptr[0])
是gdt_ptr[0])
的内存地址AD。(short *)&gdt_ptr[0])
用AD创建一个指针变量。- 这个指针变量指向一块内存。
- 这块内存占用2个字节。
- 这块内存的初始地址是
&gdt_ptr[0])
,即gdt_ptr[0])
的内存地址。 (short *)&gdt_ptr[0])
实质是指代&gdt_ptr[0]、&gdt_ptr[1]
这两小块内存。
(*)((short *)(&gdt_ptr[0]))
是&gdt_ptr[0]、&gdt_ptr[1]
这两小块内存中的值,即gdt_ptr[0]、gdt_ptr[1]
。- 为什么不需要像第二个参数一样在前面再加上一个
(void *)
?- 因为,第4步的结果是一个short类型的整型数(short能称之为整型吗?),不是内存地址,不需要强制类型转换。
- 为什么要加1?
其他
short *gdt_limit = &gdt_ptr[0];
int *gdt_base = &gdt_ptr[2];
*gdt_limit = 128 * sizeof(Descriptor) - 1;
*gdt_base = (int) &gdt;
short *gdt_limit = &gdt_ptr[0];
int *gdt_base = &gdt_ptr[2];
这段代码创建了两个变量并赋值,获取了GDT的界限和地址。可是紧接着又有下面两句,是对GDT的界限重新赋值。
*gdt_limit = 128 * sizeof(Descriptor) - 1;
*gdt_base = (int) &gdt;
这两段代码的功能重复了吗?
让我们先看另外一段代码。
#include <stdio.h>
int main(int argc, char **argv)
{
int b = 8;
printf("b = %d\n", b);
int *a = &b;
*a = 9;
printf("b = %d\n", b);
return 0;
}
执行结果是:
MacBook-Pro:my-note-book cg$ ./test
b = 8
b = 9
第8行*a = 9;
修改*a
的值,同时也修改了b
的值,因为第7行int *a = &b;
。
再回头看
short *gdt_limit = &gdt_ptr[0];
int *gdt_base = &gdt_ptr[2];
*gdt_limit = 128 * sizeof(Descriptor) - 1;
*gdt_base = (int) &gdt;
*gdt_base
指向gdt_ptr[2]
为初始地址的4个字节的连续的内存空间AD,修改*gdt_base
,实质是修改AD中的数据。int *gdt_base = &gdt_ptr[2];
的作用是让*gdt_base
指向AD;*gdt_base = (int) &gdt;
是修改AD中的数据,从业务逻辑的角度看,是把gdt的内存地址写入AD中。为什么要这样做?回忆一下我们的目的是什么?把存储了GDT的C语言中变量的内存地址存储到gdt_ptr中。
意外收获
在理解上面那个比较复杂的指针参数的过程中,我对指针有了新的理解。
int a;
,要求CPU(不知道执行者是CPU还是操作系统)为a
分配四个字节的内存空间,存储数据。
int *a;
,要求CPU为a
分配四个字节(第一片四字节内存空间,记作A),在这四个字节中存储一个内存地址,这个内存地址指向另外一个四字节的内存区域(记作B)。int *a
的含义是,指向B中的int
类型数据。
char days[5];
(short *)(&days[2]);
(short *)(&days[2]);
的含义是:
- 指向一片内存区域addr,这片内存区域的长度是连续的2个字节(short是2个字节)。
- addr的初始地址是days[2]的内存地址,所以,这片内存是
&days[2],&days[3]
。
操作系统---在内核中重新加载GDT和堆栈的更多相关文章
- iOS 解决LaunchScreen中图片加载黑屏问题
iOS 解决LaunchScreen中图片加载黑屏问题 原文: http://blog.csdn.net/chengkaizone/article/details/50478045 iOS 解决Lau ...
- 出现了内部错误-网站中X509Certificate2加载证书时出错
今天给网站配置了加密证书文件,用类X509Certificate2加载证书文件时,一直报出现了内部错误,但是Demo中用控制台程序加载证书没任何问题 读取证书文件的语句: X509Certificat ...
- iOS App中数据加载的6种方式
我们看到的APP,往往有着华丽的启动界面,然后就是漫长的数据加载等待,甚至在无网络的时候,整个处于不可用状态.那么我们怎么处理好界面交互中的加载设计,保证体验无缝衔接,保证用户没有漫长的等待感,而可以 ...
- APP中数据加载的6种方式-b
我们看到的APP,往往有着华丽的启动界面,然后就是漫长的数据加载等待,甚至在无网络的时候,整个处于不可用状态.那么我们怎么处理好界面交互中的加载设计,保证体验无缝衔接,保证用户没有漫长的等待感,而可以 ...
- 某APK中使用了动态注册BroadcastReceiver,Launcher中动态加载此APK出现java.lang.SecurityException异常的解决方法
在某APK中,通过如下方法动态注册了一个BroadcastReceiver,代码参考如下: @Override protected void onAttachedToWindow() { super. ...
- 在ASP.NET中动态加载内容(用户控件和模板)
在ASP.NET中动态加载内容(用户控件和模板) 要点: 1. 使用Page.ParseControl 2. 使用base.LoadControl 第一部分:加载模板 下 面是一个模板“<tab ...
- 在MVC应用程序中动态加载PartialView
原文:在MVC应用程序中动态加载PartialView 有时候,我们不太想把PartialView直接Render在Html上,而是使用jQuery来动态加载,或是某一个事件来加载.为了演示与做好这个 ...
- cocos2dx lua中异步加载网络图片,可用于显示微信头像
最近在做一个棋牌项目,脚本语言用的lua,登录需要使用微信登录,用户头像用微信账户的头像,微信接口返回的头像是一个url,那么遇到的一个问题就是如何在lua中异步加载这个头像,先在引擎源码里找了下可能 ...
- 【Swift】swift中懒加载的写法
swift中懒加载的写法,直接上例子 (懒加载一个遮罩视图) lazy var dummyView: UIView = { let v = UIView() v.backgroundColor = U ...
随机推荐
- 牛客53680 「金」点石成金 (dfs)
题意:给你\(n\)组数,每组4个正整数\(a,b,c,d\),每组数有两个选择: 1.增加\(a\)个财富,消耗\(b\)点魔法. 2.回复\(c\)点魔法,减少\(a\)个财富. 求最后财 ...
- Codeforces Round #479 (Div. 3) E. Cyclic Components (思维,DFS)
题意:给你\(n\)个顶点和\(m\)条边,问它们有多少个单环(无杂环),例如图中第二个就是一个杂环. 题解:不难发现,如果某几个点能够构成单环,那么每个点一定只能连两条边.所以我们先构建邻接表,然后 ...
- keras fit_generator 并行
虽然已经走在 torch boy 的路上了, 还是把碰到的这个坑给记录一下 数据量较小时,我们可直接把整个数据集 load 到内存里,用 model.fit() 来拟合模型. 当数据集过大比如几十个 ...
- 实战交付一套dubbo微服务到k8s集群(8)之configmap使用
使用ConfigMap管理应用配置 拆分环境 主机名 角色 IP地址 mfyxw10.mfyxw.com zk1.od.com(Test环境) 192.168.80.10 mfyxw20.mfyxw. ...
- MySQL 事务特征 & 隔离级别
数据库事务特征 Atomicity 原子性 事务是一个原子性质的操作单元,事务里面的对数据库的操作要么都执行,要么都不执行, Consistent 一致性 在事务开始之前和完成之后,数据都必须保持一致 ...
- Shpfile文件的字段类型说明
Shpfile文件的字段类型设置如下表所示: 字段类型 字符 字段长度 长整型 N 9 短整型 N 4 浮点型 F 13 双精度 F 19 文本 C 50 特别需要注意的是字段长度,在导出SHP的时候 ...
- python 3.7 安装 sklearn keras(tf.keras)
# 1 sklearn 一般方法 网上有很多教程,不再赘述. 注意顺序是 numpy+mkl ,然后 scipy的环境,scipy,然后 sklearn # 2 anoconda ana ...
- leetcode32 最长游戏括号 dp
有一说一,我觉得这题没有到困难级 要保存之前的状态,感觉是很明显的dp 思路和题解一样 class Solution { public: int longestValidParentheses(str ...
- 手工数据结构系列-C语言模拟队列 hdu1276
#include <stdio.h> #include <stdlib.h> #define init_size 1000 typedef struct { int head, ...
- codevs1154能量项链 环形区间DP 细节
中文题..题意略 我们知道每次枚举最后合并哪两个.. 于是枚举中间节点k 我犯的错误是将转移方程写成了,dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]+a[l]*a ...