操作系统---在内核中重新加载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 ...
随机推荐
- tesseract-ocr的安装及使用pycharm来运行
1.可以在:http://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-setup-4.00.00dev.exe 下载一个exe文件,然后直接按照提 ...
- PowerShell启用多跳远程控制
有些场景下,我们使用远程连接了某个Server,在远程Server中再想进行远程操作,就会提示错误.因为默认的认证信息只会传递一跳. 对此,微软官方文档给出的解决方案是:https://docs.mi ...
- Numpy Quickstart tutorial
此文是关于Numpy的一些基本用法, 内容来源于Numpy官网:https://docs.scipy.org/doc/numpy-dev/user/quickstart.html 1.The Basi ...
- codeforces 870C
C. Maximum splitting time limit per test 2 seconds memory limit per test 256 megabytes input standar ...
- POJ 3581 Sequence(后缀数组)题解
题意: 已知某字符串\(str\)满足\(str_1 > max\{str_2,str_3 \cdots str_n\}\),现要求把这个字符串分成连续的三组,然后每组都翻转,问字典序最小是什么 ...
- Linux 应用开发----socket编程笔记
Linux socket编程 套接字定义描述 套接字的域 AF_INET ====>IPv4 AF_INET6 ====>IPv6 AF_UNIX ====>unix 域 AF_UP ...
- 快速搞懂.NET 5/.NET Core应用程序的发布部署
.NET Framework时代,.NET 应用程序大多直接部署运行在Windows服务器上,当然也可以通过Mono部署运行在Linux上.无论部署exe,还是IIS站点.或是Windows Serv ...
- how to enable vue cli auto open the localhost url
how to enable vue cli auto open the localhost URL bad you must click the link by manually, waste of ...
- Awesome Gatsby blog websites
Awesome Gatsby blog websites very simple very clean i18n dark mode (css var) demos https://overreact ...
- React Hooks & react forwardRef hooks & useReducer
React Hooks & react forwardref hooks & useReducer react how to call child component method i ...