首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
Search
1
在IC617中进行xa+vcs数模混仿
81 阅读
2
科普:Memory Compiler生成的Register file和SRAM有何区别?
73 阅读
3
virtuoso和empyrean alps模拟仿真和混仿教程
73 阅读
4
后仿中$setup,$hold与$setuphold
44 阅读
5
文档内容搜索哪家强? 15款文件搜索软件横向评测
35 阅读
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
登录
Search
标签搜索
python
Docker
vscode
linux
systemverilog
vcs
STM32
PyQT
EDA
FPGA
gvim
cadence
Alist
xilinx
UVM
uos
macos
package
MCU
risc-v
bennyhe
累计撰写
378
篇文章
累计收到
31
条评论
首页
栏目
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
搜索到
32
篇与
的结果
2026-01-15
shell/cshell/bash 知识汇总
(1)shell获取字符串第1个字符str="/home/gateman" if [ ${str:0:1} = "/" ]; then echo "yes' fi
2026年01月15日
6 阅读
0 评论
0 点赞
2026-01-08
从操作系统内存管理来说,malloc申请一块内存的背后原理是什么?
作者:码农的荒岛求生链接:https://www.zhihu.com/question/33979489/answer/1849544189来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。对内存分配器透彻理解是编程高手的标志之一。如果你不能理解malloc之类内存分配器实现原理的话,那你可能就写不出高性能程序,写不出高性能程序就很难参与核心项目,参与不了核心项目那么很难升职加薪,很难升级加薪就无法走向人生巅峰,没想到内存分配竟如此关键,为了走上人生巅峰你也要势必读完本文。现在我们知道了,对内存分配器透彻的理解是写出高性能程序的关键所在,那么我们该怎样透彻理解内存分配器呢?还有什么能比你自己动手实现一个理解的更透彻吗?接下来,我们就自己实现一个malloc内存分配器。读完本文后内存分配对你将不再是一个神秘的黑盒。在讲解实现原理之前,我们需要回答一个基本问题,那就是我们为什么要发明内存分配器这种东西。内存申请与释放程序员经常使用的内存申请方式被称为动态内存分配,Dynamic Memory Allocation。我们为什么需要动态的去进行内存分配与释放呢?答案很简单,因为我们不能提前知道程序到底需要使用多少内存。那我们什么时候才能知道呢?答案是只有当程序真的运行起来后我们才知道。这就是为什么程序员需要动态的去申请内存的原因,如果能提前知道我们的程序到底需要多少内存,那么直接知道告诉编译器就好了,这样也不必发明malloc等内存分配器了。知道了为什么要发明内存分配器的原因后,接下来我们着手实现一个。在此之前,顺便给大家推荐一份学习算法刷题的绝技资料,除了本文讲到的底层技术,算法也不可忽视,如果想进BAT、TMD、快手这样的一线大厂,认认真真过上一遍,这些大厂算法面试一关大部分题目都不在话下:Github疯传!阿里P8大佬写的Leetcode刷题笔记,秒杀80%的算法题!mp.weixin.qq.com/s/A-HPH3Tkl8KOvZgdRArvIg程序员应如何看待内存实际上,现在这个年代的程序员是很幸福的,程序员很少去关心内存分配的问题。作为程序员,可以简单的认为我们的程序独占内存,注意,是独占哦。写程序时你从来没有关心过如果我们的程序占用过多内存会不会影响到其它程序,我们可以简单的认为每个程序(进程)独占4G内存(32位操作系统),即使我们的物理内存512M。不信你可以去试试,在即使只有512M大小的内存上你依然可以申请到2G内存来使用,可这是为什么呢?关于这个问题我们在后续文章中会详细阐述。总之,程序员可以放心的认为我们的程序运行起来后在内存中是这样的:作为程序员我们应该知道,内存动态申请和释放都发生在堆区,heap。我们使用的malloc或者C++中的new申请内存时,就是从堆区这个区域中申请的。接下来我们就要自己管理堆区这个内存区域。堆区这个区域实际上非常简单,真的是非常简单,你可以将其看做一大数组,就像这样:从内存分配器的角度看,内存分配器根本不管你是整数、浮点数、链表、二叉树等数据结构、还是对象、结构体等这些花哨的概念,在内存分配器眼里不过就是一个内存块,这些内存块中可以装入原生的字节序列,申请者拿到该内存块后可以塑造成整数、浮点数、链表、二叉树等数据结构以及对象、结构体等。我们要在这片内存上解决两个问题:实现一个malloc函数,也就是如果有人向我申请一块内存,我该怎样从堆区这片区域中找到一块返回给申请者。实现一个free函数,也就是当某一块内存使用完毕后,我该怎样还给堆区这片区域。这是内存分配器要解决的两个最核心的问题,接下来我们先去停车场看看能找到什么启示。从停车场到内存管理实际上你可以把内存看做一条长长的停车场,我们申请内存就是要找到一块停车位,释放内存就是把车开走让出停车位只不过这个停车场比较特殊,我们不止可以停小汽车、也可以停占地面积很小的自行车以及占地面积很大的卡车,重点就是申请的内存是大小不一的,在这样的条件下你该怎样实现以下两个目标呢?快速找到停车位,在内存申请中,这涉及到以最大速度找到一块满足要求的空闲内存尽最大程度利用停车场,我们的停车场应该能停尽可能多的车,在内存申请中,这涉及到在给定条件下尽可能多的满足内存申请需求现在,我们已经清楚的理解任务了,那么该怎么实现呢?任务拆分现在我们已经明确要实现什么以及衡量其好坏的标准,接下来我们就要去设计实现细节了,让我们把任务拆分一下,怎么拆分呢?我们可以自己想一下从内存的申请到释放需要哪些细节。申请内存时,我们需要在内存中找到一块大小合适的空闲内存分配出去,那么我们怎么知道有哪些内存块是空闲的呢?因此,第一个实现细节出现了,我们需要把内存块用某种方式组织起来,这样我们才能追踪到每一块内存的分配状态。现在空闲内存块组织好了,那么一次内存申请可能有很多空闲内存块满足要求,那么我们该选择哪一个空闲内存块分配给用户呢?因此,第二个实现细节出现了,我们该选择什么样的空闲内存块给到用户。接下来我们找到了一块大小合适的内存块,假设用户需要16个字节,而我们找到的这块空闲内存块大小为32字节,那么将16字节分配给用户后还剩下16字节,这剩下的内存该怎么处理呢?因此,第三个实现细节出现了,分配出去内存后,空闲内存块剩余的空间该怎么处理?最后,分配给用户的内存使用完毕,这是第四个细节出现了,我们该怎么处理用户还给我们的内存呢?以上四个问题是任何一个内存分配器必须要回答的,接下来我们就一一解决这些问题,解决完这些问题后一个崭新的内存分配器就诞生啦。管理空闲内存块空闲内存块的本质是需要某种办法来来区分哪些是空闲内存哪些是已经分配出去的内存。有的同学可能会说,这还不简单吗,用一个链表之类的结构记录下每个空闲内存块的开始和结尾不就可以了,这句话也对也不对。说不对,是因为如果要申请内存来创建这个链表那么这就是不对的,原因很简单,因为创建链表不可避免的要申请内存,申请内存就需要通过内存分配器,可是你要实现的就是一个内存分配器,你没有办法向一个还没有实现的内存分配器申请内存。说对也对,我们确实需要一个类似链表这样的结构来维护空闲内存块,但这个链表并不是我们常见的那种。因为我们无法将空闲内存块的信息保存在其它地方,那么没有办法,我们只能将维护内存块的分配信息保存在内存块本身中,这也是大多数内存分配器的实现方法。那么,为了维护内存块分配状态,我们需要知道哪些信息呢?很简单:一个标记,用来标识该内存块是否空闲一个数字,用来记录该内存块的大小为了简单起见,我们的内存分配器不对内存对齐有要求,同时一次内存申请允许的最大内存块为2G,注意,这些假设是为了方便讲解内存分配器的实现而屏蔽一些细节,我们常用的malloc等不会有这样的限制。因为我们的内存块大小上限为2G,因此我们可以使用31个比特位来记录块大小,剩下的一个比特位用来标识该内存块是空闲的还是已经被分配出去了,下图中的f/a是free/allocate,也就是标记是已经分配出去还是空闲的。这32个比特位就是header,用来存储块信息。剩下的灰色部分才是真正可以分配给用户的内存,这一部分也被称为负载,payload,我们调用malloc返回的内存起始地址正是这块内存的起始地址。现在你应该知道了吧,不是说堆上有10G内存,这里面就可以全部用来存储数据的,这里面必然有一部分要拿出来维护内存块的一些信息,就像这里的header一样。跟踪内存分配状态有了上图,我们就可以将堆这块内存区域组织起来并进行内存分配与释放了,如图所示:在这里我们的堆区还很小,每一方框代表4字节,其中红色区域表示已经分配出去的,灰色区域表示空闲内存,每一块内存都有一个header,用带斜线的方框表示,比如16/1,就表示该内存块大小是16字节,1表示已经分配出去了;而32/0表示该内存块大小是32字节,0表示该内存块当前空闲。细心的同学可能会问,那最后一个方框0/1表示什么呢?原来,我们需要某种特殊标记来告诉我们的内存分配器是不是已经到末尾了,这就是最后4字节的作用。通过引入header我们就能知道每一个内存块的大小,从而可以很方便的遍历整个堆区。遍历方法很简单,因为我们知道每一块的大小,那么从当前的位置加上当前块的大小就是下一个内存块的起始位置,如图所示:通过每一个header的最后一个bit位就能知道每一块内存是空闲的还是已经分配出去了,这样我们就能追踪到每一个内存块的分配信息,因此上文提到的第一个问题解决了。接下来我们看第二个问题。怎样选择空闲内存块当应用程序调用我们实现的malloc时,内存分配器需要遍历整个空闲内存块找到一块能满足应用程序要求的内存块返回,就像下图这样:假设应用程序需要申请4字节内存,从图中我们可以看到有两个空闲内存块满足要求,第一个大小为8字节的内存块和第三个大小为32字节的内存块,那么我们到底该选择哪一个返回呢?这就涉及到了分配策略的问题,实际上这里有很多的策略可供选择。First Fit最简单的就是每次从头开始找起,找到第一个满足要求的就返回,这就是所谓的First fit方法,教科书中一般称为首次适应方法,当然我们不需要记住这样拗口的名字,只需要记住这是什么意思就可以了。这种方法的优势在于简单,但该策略总是从前面的空闲块找起,因此很容易在堆区前半部分因分配出内存留下很多小的内存块,因此下一次内存申请搜索的空闲块数量将会越来越多。Next Fit该方法是大名鼎鼎的Donald Knuth首次提出来的,如果你不知道谁是Donald Knuth,那么数据结构课上折磨的你痛不欲生的字符串匹配KMP算法你一定不会错过,KMP其中的K就是指Donald Knuth,该算法全称Knuth–Morris–Pratt string-searching algorithm,如果你也没听过KMP算法那么你一定听过下面这本书:这就是更加大名鼎鼎的《计算机程序设计艺术》,这本书就是Donald Knuth写的,如果你没有听过这本书请面壁思过一分钟,比尔盖茨曾经说过,如果你看懂了这本书就去给微软投简历吧,这本书也是很多程序员买回来后从来不会翻一眼只是拿来当做镇宅之宝用的。不止比尔盖茨,有一次乔布斯见到Knuth老爷子后。。算了,扯远了,有机会再和大家讲这个故事,拉回来。Next Fit说的是什么呢?这个策略和First Fit很相似,是说我们别总是从头开始找了,而是从上一次找到合适的空闲内存块的位置找起,老爷子观察到上一次找到某个合适的内存块的地方很有可能剩下的内存块能满足接下来的内存分配请求,由于不需要从头开始搜索,因此Next Fit将远快于First Fit。然而也有研究表明Next Fit方法内存使用率不及First Fit,也就是同样的停车场面积,First Fit方法能停更多的车。Best FitFirst Fit和Next Fit都是找到第一个满足要求的内存块就返回,但Best Fit不是这样。Best Fit算法会找到所有的空闲内存块,然后将所有满足要求的并且大小为最小的那个空闲内存块返回,这样的空闲内存块才是最Best的,因此被称为Best Fit。就像下图虽然有三个空闲内存块满足要求,但是Best Fit会选择大小为8字节的空闲内存块。显然,从直觉上我们就能得出Best Fit会比前两种方法能更合理利用内存的结论,各项研究也证实了这一点。然而Best Fit最大的缺点就是分配内存时需要遍历堆上所有的空闲内存块,在速度上显然不及前面两种方法。以上介绍的这三种策略在各种内存分配器中非常常见,当然分配策略远不止这几种,但这些算法不是该主题下关注的重点,因此就不在这里详细阐述了,假设在这里我们选择First Fit算法。没有银弹重要的是,从上面的介绍中我们能够看到,没有一种完美的策略,每一种策略都有其优点和缺点,我们能做到的只有取舍和权衡。因此,要实现一个内存分配器,设计空间其实是非常大的,要想设计出一个通用的内存分配器,就像我们常用的malloc是很不容易的。其实不止内存分配器,在设计其它软件系统时我们也没有银弹。分配内存现在我们找到合适的空闲内存块了,接下来我们又将面临一个新的问题。如果用户需要12字节,而我们的空闲内存块也恰好是12字节,那么很好,直接返回就可以了。但是,如果用户申请12字节内存,而我们找到的空闲内存块大小为32字节,那么我们是要将这32字节的整个空闲内存块标记为已分配吗?就像这样:这样虽然速度最快,但显然会浪费内存,形成内部碎片,也就是说该内存块剩下的空间将无法被利用到。一种显而易见的方法就是将空闲内存块进行划分,前一部分设置为已分配,返回给内存申请者使用,后一部分变为一个新的空闲内存块,只不过大小会更小而已,就像这样:我们需要将空闲内存块大小从32修改为16,其中消息头header占据4字节,剩下的12字节分配出去,并将标记为置为1,表示该内存块已分配。分配出16字节后,还剩下16字节,我们需要拿出4字节作为新的header并将其标记为空闲内存块。释放内存到目前为止,我们的malloc已经能够处理内存分配请求了,还差最后的内存释放。内存释放和我们想象的不太一样,该过程并不比前几个环节简单。首先,释放内存时不需要指定大小,原因很简单,因为我们从要释放的指针地址上移就能知道该内存块的所有信息:其次我们要考虑到的关键一点就在于,与被释放的内存块相邻的内存块可能也是空闲的。如果释放一块内存后我们仅仅简单的将其标志位置为空闲,那么可能会出现下面的场景:从图中我们可以看到,被释放内存的下一个内存块也是空闲的,如果我们仅仅将这16个字节的内存块标记为空闲的话,那么当下一次申请20字节时图中的这两个内存块都不能满足要求,尽管这两个空闲内存块的总数要超过20字节。因此一种更好的方法是当应用程序向我们的malloc释放内存时,我们查看一下相邻的内存块是否是空闲的,如果是空闲的话我们需要合并空闲内存块,就像这样:在这里我们又面临一个新的决策,那就是释放内存时我们要立即去检查能否够合并相邻空闲内存块吗?还是说我们可以推迟一段时间,推迟到下一次分配内存找不到满足要的空闲内存块时再合并相邻空闲内存块。释放内存时立即合并空闲内存块相对简单,但每次释放内存时将引入合并内存块的开销,如果应用程序总是释放12字节然后申请12字节,然后在释放12字节等等这样重复的模式:free(ptr);obj* ptr = malloc(12);free(ptr);obj* ptr = malloc(12);...那么这种内存使用模式对立即合并空闲内存块这种策略非常不友好,我们的内存分配器会有很多的无用功。但这种策略最为简单,在这里我们依然选择使用这种简单的策略。实际上我们需要意识到,实际使用的内存分配器都会有某种推迟合并空闲内存块的策略。高效合并空闲内存块合并空闲内存块的故事到这里就完了吗?问题没有那么简单。让我们来看这样一个场景:使用的内存块其前和其后都是空闲的,在当前的设计中我们可以很容易的知道后一个内存块是空闲的,因为我们只需要从当前位置向下移动16字节就是下一个内存块,但我们怎么能知道上一个内存块是不是空闲的呢?我们之所以能向后跳是因为当前内存块的大小是知道的,那么我们该怎么向前跳找到上一个内存块呢?还是我们上文提到的Donald Knuth,老爷子提出了一个很聪明的设计,我们之所以不能往前跳是因为不知道前一个内存块的信息,那么我们该怎么快速知道前一个内存块的信息呢?Knuth老爷子的设计是这样的,我们不是有一个信息头header吗,那么我们就在该内存块的末尾再加一个信息尾,footer,footer一词用的很形象,header和footer的内容是一样的。因为上一内存块的footer和下一个内存块的header是相邻的,因此我们只需要在当前内存块的位置向上移动4直接就可以等到上一个内存块的信息,这样当我们释放内存时就可以快速的进行相邻空闲内存块的合并了。收工至此,我们的内存分配器就已经设计完毕了。我们的简单内存分配器采用了First Fit分配算法;找到一个满足要求的内存块后会进行切分,剩下的作为新的内存块;同时当释放内存时会立即合并相邻的空闲内存块,同时为加快合并速度,我们引入了Donald Knuth的设计方法,为每个内存块增加footer信息。这样,我们自己实现的内存分配就可以运行起来了,可以真正的申请和释放内存。总结本文从0到1实现了一个简单的内存分配器,但不希望这里的阐述给大家留下内存分配器实现很简单的印象,实际上本文实现的内存分配器还有大量的优化空间,同时我们也没有考虑线程安全问题,但这些都不是本文的目的。本文的目的在于把内存分配器的本质告诉大家,对于想理解内存分配器实现原理的同学来说这些已经足够了,而对于要编写高性能程序的同学来说实现自己的内存池是必不可少的,内存池实现也离不开这里的讨论。
2026年01月08日
2 阅读
0 评论
0 点赞
2026-01-07
怎么搭建学习Linux内核的运行、调试环境?
作者:Tools boy链接:https://www.zhihu.com/question/66594120/answer/355840304来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。一步一步教你搭建linux内核调试环境,因为毕设跟os有关,最近在学习ulk3(深入理解linux内核第三版),感觉光看书不动手实践是很容易遗忘知识,于是开始动手调试书上对应版本的linux2.6内核环境: ubuntu-14.04.1 32位桌面版 内核版本:3.13.0-32-generic 调试的内核为:linux-2.6.26版本调试工具:qemu,gdb,busybox 1.编译x86下的linux2.6内核首先下载linux-2.6.26版本内核Index of /pub/linux/kernel/v2.6/. ubuntu安装qemusudo apt-get install qemutar -zvxf linux-2.6.26.tar.gzsudo apt-get install libncurses5-dev (执行linux+busybox菜单式的配置,需要的库)进入linux2.6-26根目录,执行以下命令make menuconfig,选择 kernel hacking—>[*] compile the kernel with debug info 让其携带调试信息make defconfig 生成配置文件make 编译kernel接着make时,会出现 undefined reference to __mutex_lockslowpath解决方法:需要去kernel/目录找到mutex.c修改,在static跟void之间加上__used,同样__mutex_unlockslowpath也要加上__usedstatic __used void fastcall noinline __sched __mutex_lock_slowpath继续make,会出现以下错误gcc: error: elf_i386: No such file or directorymake[1]: * [arch/x86/vdso/vdso32-int80.so.dbg] Error 1make: * [arch/x86/vdso] Error 2原因在于我的宿主机是gcc 4.8.2,编译器版本过高,不再支持 linker-style 架构.需要修改arch/x86/vdso/Makefile,大约在28,29行 找到 VDSO_LDFLAGS_vdso.lds = -m elf_x86_64 -Wl,-soname=linux-vdso.so.1 \ -Wl,-z,max-page-size=4096 -Wl,-z,common-page-size=4096 把"-m elf_x86_64" 替换为 "-m64"2然后再继续找,大约在72行左右,找到VDSO_LDFLAGS_vdso32.lds = -m elf_i386 -Wl,-soname=linux-gate.so.1中的 "-m elf_i386" 替换为 "-m32"接着继续执行make,就可以继续编译内核,出现以下字样就表示内核编译ok了Root device is (8, 1)Setup is 12160 bytes (padded to 12288 bytes).System is 2653 kBCRC ae462731Kernel: arch/x86/boot/bzImage is ready (#4) Building modules, stage 2. MODPOST 1 modules2.接着使用busybox制作文件镜像建立目标根目录映像dd if=/dev/zero of=myinitrd4M.img bs=4096 count=1024mke2fs myinitrd4M.imgmkdir rootfssudo mount -o loop myinitrd4M.img rootfs准备dev目录sudo mkdir rootfs/devlinux启动过程中会启用 console设备sudo mknod rootfs/dev/console c 5 1另外需要提供一个linux根设备,我们使用 ramsudo mknod rootfs/dev/ram b 1 0接着下载busyboxhttps://www.busybox.net/downloads/busybox-1.20.1.tar.bz2 执行以下命令make defconfigmake menuconfig修改如下配置:busybox settings –> build options –> build busyboxas a static binary( no share libs)执行make在busybox目录下sudo make CONFIG_PREFIX=(path to rootfs)/ installsudo umount rootfs完成根文件系统的制作接着执行qemu-system-i386 -kernel ../linux-2.6.26/arch/x86/boot/bzImage -initrd myinitrd4M.img -append "root=/dev/ram init=/bin/ash"看是否可以顺利的进入到busybox提供的shell环境作者:Tools boy链接:https://www.zhihu.com/question/66594120/answer/355840304来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。3.qemu+gdb调试内核首先执行以下指令(就上条指令后面+ -s -S)qemu-system-i386 -kernel ../linux-2.6.26/arch/x86/boot/bzImage -initrd myinitrd4M.img -append "root=/dev/ram init=/bin/ash" -s -S可以看到在新打开的qemu虚拟机上,整个是一个黑屏,此时qemu在等待gdb的链接接着开启另外一个终端,执行gdb接着在gdb界面中 targe remote之前加载符号表file ../linux-2.6.26/vmlinux target remote: 1234 则可以建立gdb和gdbserver之间的连接.很多教程说接下来直接b startkernel,然后就可以调试内核了,然而我一直没有成功,这个bug卡了我两天,一直无法断点,后来查了英文资料得知(gdbserver inside qemu does not stop on breakpoints),一开始必须使用硬件断点hk startkernel才可以断点接着在start_kernel下硬件断点 hk startkernel(,普通的break断点无法使得gdb在断点处运行,这是一个gdb的bug(),接下来就可以愉快的调试内核了
2026年01月07日
2 阅读
0 评论
0 点赞
2026-01-04
计算机自制操作系统(Linux篇)一:用Nasm重写Linux引导启动程序
一、 背景Linux引导启动程序就是一台裸机从开机到出现操作系统启动运行的过程,它由3个汇编程序组成:bootsect.S、setup.S和head.s。它们之间的关系是:学习Linux操作系统的第一大难点就是引导启动程序。很多人一进Linux的门可能就倒在了这里,因为要完全读懂这3个汇编程序还是有点困难,尤其是对于长期从事高级语言软件开发的人员来说。这个问题,除了读者水平高低以外,个人觉得这3个汇编程序写得确实不怎么样,程序模块化程度不够,因此可读性不好。另外,有些程序还是AT& T汇编,这个让我无法接受,也没有读下去的动力。于是,我决定自己动手,用自己最喜欢和熟悉的Nasm重新写出Linux的三个引导启动程序,原则就是尽量保持Linux源码模样的基础上提升可读性:做到引导启动程序每一步在干什么,都有明确的屏幕输出提示。二、引导程序启动过程3个汇编程序在启动设备中的原始位置关系如图:bootsect.S代码是磁盘引导块程序,驻留在磁盘的第一个扇区中(引导扇区,0磁道(柱面),0磁头,第1个扇区)。实际就是我们之前说过的MBR。setup模块占用随后的4个扇区,逻辑扇区号:2-5。system模块大约占随后的260个扇区,逻辑扇区号从6开始,大约需要读8个柱面。3个汇编程序在启动过程中,它们在内存中的位置关系随时间而变化如图:这是当年Linus设计的过程,我们接下来的任务就来严格按照这个过程进行程序编写。三、 bootsect程序在bootsect代码执行期间,它会将自己移动到内存绝对地址0x90000开始处并继续执行。该程序的主要作用是首先把从磁盘第2个扇区开始的4个扇区的setup模块(由setup.s编译而成)加载到内存紧接着bootsect后面位置处(0x90200),最后把磁盘上setup模块后面的system模块加载到内存0x10000开始的地方。随后确定根文件系统的设备号。具体到程序,需要我们依次来实现以下核心功能:将bootsect程序本身从0x07c0复制到0x9000(共1个扇区512B)完成复制后,程序中有一个设计比较巧妙的地方:jmp INITSEG:gogo: mov ax,cs 这就是为什么要把bootsect程序要自身复制到0x9000的原因,因为完成复制且执行跳转指令jmp INITSEG:go后,程序才能刚好从go这个地方继续执行。但是,我个人理解这一步并不是必须的。可以设计成先将setup读到0x9020,然后直接从MBR内跳转到setup(0x9020)即可,不清楚当初设计复制MBR这段代码到0x9000的意义何在。当然在Linux后面的内核程序中,也用到了在MBR中存储的ROOT_DEV和SWAP_DEV内存数据,所以这么做也有一定的道理。将setup程序装载到0x9020共4个扇区4*512B,这是一个从启动设备读取数据到内存的过程。3.将system程序装载到0x1000共260个扇区260*512B,这也是一个从启动设备读取数据到内存的过程。跳转到setup程序所在的目标地址:0x9020这是bootsect程序执行完成之后,最后的去向。bootsect程序里面一个比较重要的东西就是如何通过BIOS中断来读取软盘数据到内存,这个在之前我的专栏文章中有详细描述,因此本程序可以直接使用:kaka:计算机自制操作系统(三):读写磁盘操作36 赞同 · 15 评论 文章四、setup程序setup的主要作用是利用ROM BIOS中断读取机器系统数据,并将这些数据保存到0x90000开始的位置(覆盖掉了bootsect程序所在的地方)。然后setup程序将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x00000处。接下来就是加载中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR),开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20 -0x2f。最后设置CPU的控制寄存器CR0(也称机器状态字),进入32位保护模式运行,并跳转到位于system模块最前面部分的head.s程序继续运行。为了能让 head在32位保护模式下运行, 在本程序中临时设置了中断描述符表(IDT)和全局描述 符表(GDT),并在GDT中设置了当前内核代码段的描述符和数据。后面在head中会根据内核的需要重新设置这些描述符表。具体到程序,setup模块需要依次实现以下核心功能:取扩展内存的大小值(KB)。检查显示方式(EGA/VGA)并取参数。取显示卡当前显示模式。取第一个硬盘的信息(复制硬盘参数表)。取第2个硬盘的信息(复制硬盘参数表)。检查系统是否有第2个硬盘,如果没有则把第2个表清零。将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x00000处。8.准备进入32位保护模式:(1) 初始化8259A芯片(2) 打开A20地址线(3) 设置GDT,IDT表(4) 打开保护模式开关9.进入32位保护模式运行完setup程序之后,相关的全局系统信息就全部保持在内存之中了,请记住这张图:五、 head程序head.s程序在被编译生成目标文件后会与内核其他程序的目标文件一起被链接成system模块,并位于system模块的最前面开始部分。这也就是为什么称其为头部(head)程序的原因。system模块将被放置在磁盘上setup模块之后开始的扇区中,即从磁盘上第6个扇区开始放置。具体到程序,system模块中的head程序需要依次实现以下核心功能:重置GDT实际上新设置的GDT表与原来在setup.s程序中设置的GDT表描述符除了在段限长上有些区别以外(原为8MB,现为16MB),其他内容完全一样。这里重新设置GDT的主要原因是为了把GDT表放在内存内核代码比较合理的地方。而前面设置的GDT表处于内存0x902XX处。这个地方将在内核初始化后用作内存高速缓冲区的一部分。需要注意的是,一旦重置GDT表之后,需要一句修改CS值的指令(我在程序中采用的指令是jmp AA:BB)才能触发新的GDT表数据生效。而这在Linux源码中是没有的,个人觉得这多少是一个不太合理的地方,所幸并没有引起问题,但这并不是一个好的习惯。重置IDT由于目前不需要精确制作各个中断源的中断服务,因此Linux让所有的256中断都指向这个统一的中断服务程序---称为哑中断,也即建立了256个一样的中断描述符在IDT中。我编写的程序在Linux的基础上,增加了一个IDT验证测试的环节:(1) 先是打开键盘中断,然后运行程序在死循环过程中时按下键盘,如果屏幕能正常打印提升信息,则表面外部中断体系正常。(2) 直接进行系统调用测试:int 0xXX.虽然进入保护模式后不在支持BIOS调用,但是CPU本身是支持int 0xXX指令的,它能出发CPU根据中断向量去中断表IDT中找寻相应的中断服务。因此 如果屏幕能正常打印提示信息,则表面外部中断体系正常。确认A20地址线开启我不知道是否必须的,但是Linux源码中有,所以我也一并写了。分页存储:设置页目录和页表这部分内容在之前我自己制作的操作系统中并没有涉及,因此这里特别重点学习一下。由于Linux支持虚拟存储技术,因此必须要用到分页存储。我之前自制的操作系统没有打开分页开关,因此实际访问内存的时候,线性地址=物理地址,现在用到了分页技术之后,线性地址到物理地址的映射还需要一次转换。而这个转换的过程需要操作系统进行设置和控制。有了分页机制之后,寻找物理地址的全过程就变成下面的方式:看起来很复杂,其实只要多动手实践,是完全能理解和掌握的。但是如果只是靠反复看书来理解的话,确实有点抽象。所以,动手是关键。为什么要用分页机制?分段机制在区分代码段和数据段等场景下非常适合,但是如果存在内存需要存放大量数据的情况下,分段机制就不太合理,因为分段首先是需要注册段、定义段属性等相当繁琐。最主要的问题是分段机制下,段空间大小不好控制,每启用一个数据段,无论大小,都要先定义好段的界限。分页机制的核心思想是把先把内存划分为大小固定(4KB)的一小块一小块集合,当有内存占用需求时,以这个小块为单位,需要多少块来直接按需分配内存,这样就很容易控制。我们把这个大小固定(4KB)的一小块内存就叫页。具体到32计算机,一共可寻址的物理内存是4GB,故一共可以划分为4GB/4KB=1048576页。为了表示这些内存页的占用情况,我们需要在内存中专门开辟一段空间来映射每个页的物理地址,如果说一个页用4个字节长度来映射的话,1048576页就需要1048576*4B=4MB。没错, 这段空间很大, 要占用相当一部分空间, 考虑到在实践中, 没有哪个任务会真的用到所有表项, 充其量只是很小一部分, 这就很浪费了. 为了解决这个问题, 处理器设计了层次化分页结构.分页结构层次化的主要手段是不采用单一映射表, 取而代之的是页目录表和页表的二级结构。页目录和页表可以分别设置成最大容量为1024个单元,这样一来:1048576=1024*1024,一样的可以寻址到所有的页索引。32位寻址的物理内存被划分为4KB(12bit)的页之后,寻址物理地址位数就缩短为20bit(32-12),因此理论上讲页表中的每个页表项用大约3个字节(24bit)来寻址都是可以的。但实际中,还需要定义一些页表项的属性,因此每个页表项长度一般就拓展为4个字节。这样一来,一个页表最大空间是:10244B=4KB。一共页表可以有1024个页表项,因此一个页表的最大可寻址空间是:10244KB=4MB。而页目录中各单元指向的是各页表的物理地址,由于各页表的大小是4KB,因此页表的最大物理地址编号是:4GB/4KB=2^20。同理,理论上讲页目录每个索引项用大约3个字节(24bit)都可以了。但实际中,也需要定义一些页表目录属性,因此长度也拓展为4个字节。所以,页目录的大小是:1024*4B=4KB。这个4字节长度页目录表项和页表项格式见图所示。其中P是页面存在于内存标志;R/W是读写标志;U/S是用户/超级用户标志;A是页面已访问标志;D是页面内容已修改标志;最左边20比特是表项对应页面在物理内存中页面地址的高20比特位。这里每个表项的属性标志都被设置成0x07(P=1、U/S=1、R/W=1),表示该页存在、用户可读写。划分为二级结构后,页目录、页表和物理地址之间的关系如下图:以上内容就是整个分页机制原理。具体到Linux内核0.12只用了4个页表,所以只需在页目录中设置4项。由于每个页表可寻址4MB,故总共可以寻址的空间是4*4MB=16MB。head程序初始化页目录和页表后,每个页表项和物理内存从低到高的顺序保持一一映射:Linux将页目录表放在绝对物理地址0开始处(也是本程序所处的物理内存位置,因此这段程序已执行部分将被覆盖掉),紧随后面会放置共可寻址16MB内存的4个页表,并分别设置它们的表项。在这部分内容中,我和Linux源码的设计也有点不同。Linux是把head程序的前面部分放置在物理地址0开始的地方,然后在中间0x1000-0x5000放置页表,0x5400后再放head程序的后面部分。中间用了一连串的org来定位,搞得人眼花缭乱,要理解它这段设计的分布过程还是需要较高汇编水平的。而我采用的方法是直接把head程序全部放在0x5400后面(除了物理地址0开始的一条jmp指令),所以我的程序一开始上来就直接将0x1000-0x5400全部填充0,简单明了,更易理解。转入C程序main()函数入口方法是利用返回指令ret将预先放置在堆栈中的/init/main.c程序的入口地址弹出,去运行C程序main()函数。我在程序中用下标_main人造了一个假的C语言main程序,因此暂时不需要写出C程序。head程序执行结束之后,整体内存的分布如下图:我们单独来看一下,此时Linux操作系统中非常重要GDT表分布情况:这个图在我们之前的自制操作系统专栏中有所涉及,相信到这里后会体会更深。六、 程序调试Linux中,3个引导启动程序最终生成操作系统映像的过程为:现在,我们只是实验,不需要借助于Build工具,我这次生成操作系统映像文件的过程为:1.bootsect:大小1个扇区,512B,汇编后的机器文件为bootsect.bin。汇编命令:NASM bootsect.asm -o bootsect.bin2.setup:大小4个扇区,4*512B=2KB,汇编后的机器文件为setup.bin。汇编命令:NASM setup.asm -o setup.bin在setup.asm程序中,不足2KB的地方用汇编指令填充即可,保证最终的setup.bin是2KB。3.system:大小260个扇区,260*512B=130KB。system模块的首部程序head.asm汇编后的机器文件为head.bin。汇编命令:NASM head.asm -o head.bin4.生成映像文件由于以上3个程序在启动设备上是连续存放的,因此我们使用DOS的二进制连接命令copy便可将所有的机器代码连接在一起,即可生成Linux映像文件:copy /B bootsect.bin+setup.bin+head.bin Linux.img我们把所有的命令过程写成makefile,并在生成映像文件后自动启动虚拟机Vmware就是:default : make.exe -r Linux.img bootsect.bin : bootsect.asm MakefileNASM bootsect.asm -o bootsect.bin -l bootsect.lst setup.bin : setup.asm MakefileNASM setup.asm -o setup.bin -l setup.lst head.bin : head.asm MakefileNASM head.asm -o head.bin -l head.lst Linux.img: bootsect.bin setup.bin head.bin Makefilecopy /B bootsect.bin+setup.bin+head.bin Linux.img run :make.exe VMWARE -x ../../VWare/Linux/Linux.vmx最终,计算机启动之后,我们可以清晰的看到Linux三个引导程序的启动过程:我改写的程序除了实时打印当前启动进度详细信息外,还在不同的阶段用了不同颜色的字符串进行区分。PS:软盘的最大柱面编号应是18。为了能在一个屏幕范围内装下打印信息,我截屏的时候,在程序中临时将最大柱面编号设置成了7,所以图中显示的柱面循环数据是6,7,1,2,3,4,5,6,7...。实际上,真实的柱面循环数据应该是:6,7,8,9,10,11,12,13,14,15,16,17,18,1,2,3,4,5,6,7,8,9......七、 总结通过重写Linux的引导启动程序,给我感受最深的就是:进入保护模式后,将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x00000处。这个设计非常的巧妙,因为这样做之后,好处会非常的多比如:1.所有的段寄存器(CS\DS\ES\FS\GS\SS)都可以把基址设置成0。2.所有的内存数据访问:段内偏移=绝对地址(因为段基址=0),寻址就太方便了。3.基于以上情况,汇编程序和C程序能直接进行链接耦合,各段寄存器无需重新设置段值。详细原理在之前自制操作系统中已有阐述。八、 Nasm程序源代码用Nasm改写的Linux三个引导启动程序,均为本人亲自编写并真机验证通过。为了长时间后都能读懂程序,在本次改写过程中,我对3个源程序几乎做了不留死角的详细备注。头文件config.inc;%define debug ;不注释调试,注释用于生产%ifdef debugisdebug equ 1%elseisdebug equ 0%endifDEF_INITSEG equ 0x9000 ;MBR程序挪动后的目标地址DEF_SYSSEG equ 0x1000 ;SYSEM模块放置地址DEF_SETUPSEG equ 0x9020 ;SETUP模块放置地址2.bootsect.asm;*;*Linux操作系统Nasm引导程序:bootsect,制作者:Mr.Jiang;2020-10-17*%include "config.inc"SETUPLEN equ 4 ;SETUP模块长度扇区数 BOOTSEG equ 0x07c0 ;MBR启动地址 INITSEG equ DEF_INITSEG ;MBR程序挪动后的目标地址0x9000 SETUPSEG equ DEF_SETUPSEG ;SETUP模块放置地址0x9020 SYSSEG equ DEF_SYSSEG ;SYSEM模块放置地址0x1000SETUPSector equ 2 ;SETUP开始扇区号SYSSector equ SETUPSector+SETUPLEN ;SYSTEM开始扇区号6SYScylind equ 7 ;SYSTEM读到的柱面数(8*36>260扇区);root_dev定义在引导扇区508,509字节处;当编译内核时,你可以在Makefile文件中指定自己的值。内核映像文件Image的;创建程序tools/build会使用你指定的值来设置你的根文件系统所在设备号。ROOT_DEV equ 0 ;根文件系统设备使用与系统引导时同样的设备(不指定);SWAP_DEV equ 0 ;交换设备使用与系统引导时同样的设备(不指定);;设备号=主设备号*256 + 次设备号(也即dev_no = (major<<8) + minor );主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道);0x300 - /dev/hd0 - 代表整个第1个硬盘;;0x301 - /dev/hd1 - 第1个盘的第1个分区;;…;0x304 - /dev/hd4 - 第1个盘的第4个分区;;0x305 - /dev/hd5 - 代表整个第2个硬盘;;0x306 - /dev/hd6 - 第2个盘的第1个分区;;…;0x309 - /dev/hd9 - 第2个盘的第4个分区;;次设备号 = type*4 + nr,其中;nr为0-3分别对应软驱A、B、C或D;type是软驱的类型(2:1.2MB或7:1.44MB等)。;因为7*4+0=28=0x1c,所以/dev/PS0 指的是1.44MB A驱动器,其设备号是0x021cjmp startstart: mov ax,0 ;BIOS把引导扇区加载到0x7c00时,ss=0x00,sp=0xfffe mov ss,ax mov sp,BOOTSEG ;重新定义堆栈0x7c00 mov ax,BOOTSEG mov ds,ax ;为显示各种提示信息做准备 mov si, welcome call showmsg ;打印"Linux" ;0x021c :/dev/PS0 - 1.44Mb 软驱A盘 mov word [root_dev],0x021c ;不指定,将软驱A设置成根文件系统设备保存在root_dev ;1.将bootsect程序从0x07c0复制到0x9000(共1个扇区512B) mov ax, INITSEG mov es,ax mov cx, 256 sub si,si sub di,di rep ;循环挪动次数=512B/16B movsw ;一次挪动16B, jmp INITSEG:go ;完成复制后,CPU将会跳转到这里 go: mov ax,cs ;到新的段地址后重新设置DS mov ds,ax ;为显示各种提示信息做准备 mov si, msg1 call showmsg ;打印必要信息 mov ax,cs ;重新定义堆栈,栈顶:0x9ff00-12(参数表长度=0x9fef4 mov ss,ax ;因为栈顶后面安排了一个长度12的自建驱动器参数表 mov sp,0xfef4 ;刨除掉SS段值0x9000*10后,SP的偏移量是0xfef4 ;2.将setup程序装载到0x9020(共4个扇区4*512B) mov si, msg2 call showmsg mov ax, SETUPSEG ;设置setup装载到的目标段地址 mov es,ax ;设置setup装载到的目标段地址 mov byte [sector+11],SETUPSector ;设置开始读取的扇区号:2 call loadsetup ;3.将system程序装载到0x1000(共240个扇区4*512B) mov si, msg3 call showmsg mov ax, SYSSEG ;设置system装载到的目标段地址 mov es,ax ;设置system装载到的目标段地址 mov byte [sector+11],SYSSector ;设置开始读取的扇区号:6 call loadsystem ;jmp $ ;调试 jmp SETUPSEG:0 ;bootsect运行完毕,跳到setup:0x9020 showmsg: ;打印字符串子程序 call newline call printstr call newline ret ;读软盘逻辑扇区2-5共4个扇区 loadsetup: call read1sector MOV AX,ES ADD AX,0x0020 ;一个扇区占512B=200H,刚好能被整除成完整的段 MOV ES,AX ;因此只需改变ES值,无需改变BX即可。 inc byte [sector+11] ;读完一个扇区 cmp byte [sector+11],SETUPLEN+1+1 ;读到的结束扇区 jne loadsetup ret ;读软盘逻辑扇区6-8*36共282个扇区loadsystem: call read1sector MOV AX,ES ADD AX,0x0020 ;一个扇区占512B=200H,刚好能被整除成完整的段 MOV ES,AX ;因此只需改变ES值,无需改变BX即可。 inc byte [sector+11] ;读完一个扇区 cmp byte [sector+11],18+1 ;最大扇区编号18, jne loadsystem mov byte [sector+11],1 inc byte [header+11] ;读完一个磁头 cmp byte [header+11],1+1 ;最大磁头编号1 jne loadsystem mov byte [header+11],0 inc byte [cylind+11] ;读完一个柱面 cmp byte [cylind+11],SYScylind+1 jne loadsystem ret numtoascii: ;将2位数的10进制数分解成ASII码才能正常显示。 ;如柱面56 分解成出口ascii: al:35,ah:36 mov ax,0 mov al,cl ;输入cl mov bl,10 div bl add ax,3030h ret readinfo: ;实时显示当前读到哪个扇区、哪个磁头、哪个柱面 mov si,cylind call printstr mov si,header call printstr mov si,sector call printstr ret read1sector: ;读1扇区通用程序。扇区参数由 sector header cylind控制 mov cl, [sector+11] ;为了能实时显示读到的物理位置 call numtoascii mov [sector+7],al mov [sector+8],ah mov cl,[header+11] call numtoascii mov [header+7],al mov [header+8],ah mov cl,[cylind+11] call numtoascii mov [cylind+7],al mov [cylind+8],ah MOV CH,[cylind+11] ;柱面开始读 MOV DH,[header+11] ;磁头开始读 mov cl,[sector+11] ;扇区开始读 call readinfo ;显示软盘读到的物理位置 mov di,0retry: MOV AH,02H ; AH=0x02 : AH设置为0x02表示读取磁盘 MOV AL,1 ; 要读取的扇区数 mov BX, 0 ; ES:BX表示读到内存的地址 MOV DL,00H ; 驱动器号,0表示软盘A,硬盘C:80H C 硬盘D:81H INT 13H ; 调用BIOS 13号中断,磁盘相关功能 JNC READOK ; 未出错则跳转到READOK,出错的话EFLAGS的CF位置1 inc di MOV AH,0x00 MOV DL,0x00 ; A驱动器 INT 0x13 ; 重置驱动器 cmp di, 5 ; 软盘很脆弱,同一扇区如果重读5次都失败就放弃 jne retry mov si, Fyerror call printstr call newline jmp exitreadREADOK: mov si, FloppyOK call printstr call newlineexitread: ret printstr: ;显示指定的字符串, 以'$'为结束标记 mov al,[si] cmp al,'$' je disover mov ah,0eh int 10h inc si jmp printstrdisover: ret newline: ;显示回车换行 mov ah,0eh mov al,0dh int 10h mov al,0ah int 10h ret welcome db '(i)Linux-bootsect!','$'msg1 db '1.bootsect to 0x9000','$'msg2 db '2.setup to 0x9020','$'msg3 db '3.system to 0x1000','$'cylind db 'cylind:?? $',0 ; 设置开始读取的柱面编号header db 'header:?? $',0 ; 设置开始读取的磁头编号sector db 'sector:?? $',1, ; 设置开始读取的扇区编号FloppyOK db '-Floppy Read OK','$'Fyerror db '-Floppy Read Error' ,'$'times 512-2*3-($-$$) db 0 ;MBR程序中间部分用0填充 swap_dev:dw SWAP_DEV ;2Byte,存放交换系统所在设备号(init/main.c中会用)。root_dev:dw ROOT_DEV ;2Byte,存放根文件系统所在设备号(init/main.c中会用)。 boot_flag: db 0x55,0xaa ;2Byte,MBR启动标记 3.setup.asm;;*Linux操作系统Nasm引导程序:setup,制作者:Mr.Jiang;2020-10-20%include "config.inc"INITSEG EQU DEF_INITSEG ;全部同bootsect SYSSEG EQU DEF_SYSSEG SETUPSEG EQU DEF_SETUPSEG jmp startstart: mov ax,SETUPSEG mov ds,ax ;为显示各种提示信息做准备 mov si, welcome call showmsg ;打印"Welcome Linux" mov ax,INITSEG mov es,ax ;将setup模块的各类数据保存在0x9000?处 ;1.取扩展内存的大小值(KB) mov si, msg1 call showmsg mov ah,0x88 int 0x15 ;通过调用BIOS中断实现 mov [es:2],ax ;将扩展内存数值存在0x90002处(1个字)。 ;2.检查显示方式(EGA/VGA)并取参数。 mov si, msg2 call showmsg mov ah,0x12 mov bl,0x10 int 0x10 mov [es:8],ax mov [es:10],bx ;0x9000A =安装的显示内存;0x9000B=显示状态(彩/单色) mov [es:12],cx ;0x9000C =显示卡特性参数。 mov ax,0x5019 ;在ax中预置屏幕默认行列值(ah = 80列;al=25行)。 mov [es:14],ax ;保存屏幕当前行列值(0x9000E,0x9000F)。 mov ah,0x03 ;取屏幕当前光标位置 xor bh,bh int 0x10 mov [es:0],dx ;保存在内存0x90000处(2字节) ;3.取显示卡当前显示模式 mov si, msg3 call showmsg mov ah,0x0f int 0x10 mov [es:4],bx ;0x90004(1字)存放当前页 mov [es:6],ax ;0x90006存放显示模式;0x90007存放字符列数。 ;4.取第一个硬盘的信息(复制硬盘参数表)。 ;第1个硬盘参数表的首地址竟然是中断0x41的中断向量值 ;而第2个硬盘参数表紧接在第1个表的后面,中断0x46的向量向量值 ;也指向第2个硬盘的参数表首址。表的长度是16个字节。 mov si, msg4 call showmsg push ds ;由于复制数据要修改DS的值,因此暂存起来 mov ax,0x0000 mov ds,ax lds si,[4*0x41] mov ax,INITSEG mov es,ax mov di,0x0080 ;0x90080处存放第1个硬盘的表 mov cx,0x10 rep movsb ;5.取第2个硬盘的信息(复制硬盘参数表)。 pop ds ;恢复DS为本段setup段地址,才能正常打印字符串 mov si, msg5 call showmsg push ds ;由于复制数据要修改DS的值,因此暂存起来 mov ax,0x0000 mov ds,ax lds si,[4*0x46] mov ax,INITSEG mov es,ax mov di,0x0090 ;0x90090处存放第2个硬盘的表 mov cx,0x10 rep movsb ;6.检查系统是否有第2个硬盘。如果没有则把第2个表清零。 pop ds ;恢复DS的值,才能正常打印字符串 mov si, msg6 call showmsg mov ax,0x01500 mov dl,0x81 int 0x13 jc no_disk1 cmp ah,3 je is_disk1no_disk1:mov ax,INITSEG mov es,ax mov di,0x0090 mov cx,0x10 mov ax,0x00 rep stosbis_disk1:;7.现在要进入保护模式了 mov si, msg7 call showmsg mov si, msg8 call showmsg mov cx,14 line: call newline ;循环换行,清除一些屏幕显示 loop line cli ;禁用16位中断 ;8.将system模块移到正确的位置。 ;bootsect引导程序会把 system 模块读入到内存 0x10000(64KB)开始的位置 ;下面这段程序是再把整个system模块从 0x10000移动到 0x00000位置。即把从 ;0x10000到0x8ffff 的内存数据块(512KB)整块地向内存低端移动了64KB字节。 call mov_system ;会覆盖实模式下的中断区,BIOS中断再也无法使用 ;9.装载寄存器IDTR和GDTRmov ax,SETUPSEG ;ds指向本程序(setup)段 mov ds,ax lidt [idt_48] ;加载IDTR lgdt [gdt_48] ;加载GDTR ;10.现开启A20地址线 call empty_8042 ;8042状态寄存器,等待输入缓冲器空。 ;只有当输入缓冲器为空时才可以对其执行写命令。 mov al,0xD1 ;0xD1命令码-表示要写数据到 out 0x64,al ;8042的P2端口。P2端口位1用于A20线的选通。 call empty_8042 ;等待输入缓冲器空,看命令是否被接受。 mov al,0xDF ;A20 on ! 选通A20地址线的参数。 out 0x60,al ;数据要写到0x60口。 call empty_8042 ;若此时输入缓冲器为空,则表示A20线已经选通。 ;11.设置8259A中断芯片,即int 0x20--0x2F call set_8259A ;12.打开保护模式PE开关 mov ax,0x0001 ;保护模式比特位(PE) lmsw ax ;就这样加载机器状态字! ;13.跳转触发到32位保护模式代码 ;jmp dword 1*8:inprotect+SETUPSEG*0x10 ;保护模式下的段基地址:0x90200 ;这句是调试验证进入保护模式后系统是否正常 jmp dword 1*8:0 ;setup程序到此结束 ;跳转到0x00000,也即system程序(head.asm) ;把整个system模块从 0x10000移动到 0x00000位置。 mov_system: mov ax,0x0000 cld ;'direction'=0, movs moves forwarddo_move:mov es,ax ;es:di是目的地址(初始为0x0:0x0) add ax,0x1000 cmp ax,0x9000 ;已把最后一段(从0x8000段开始的64KB)移动完? jz end_move mov ds,ax ;ds:si是源地址(初始为0x1000:0x0) sub di,di sub si,si mov cx,0x8000 ;移动0x8000字(64KB字节)。 rep movsw jmp do_moveend_move: ret;设置8259A中断芯片 set_8259A: mov al,0x11 out 0x20,al dw 0x00eb,0x00eb ;jmp $+2, jmp $+2 out 0xA0,al dw 0x00eb,0x00eb mov al,0x20 ;Linux系统硬件中断号被设置成从0x20开始 out 0x21,al dw 0x00eb,0x00eb mov al,0x28 ;start of hardware int's 2 (0x28) out 0xA1,al dw 0x00eb,0x00eb mov al,0x04 ;8259-1 is master out 0x21,al dw 0x00eb,0x00eb mov al,0x02 ;8259-2 is slave out 0xA1,al dw 0x00eb,0x00eb mov al,0x01 ;8086 mode for both out 0x21,al dw 0x00eb,0x00eb out 0xA1,al dw 0x00eb,0x00eb mov al,0xFF ;屏蔽主芯片所有中断请求。 out 0x21,al dw 0x00eb,0x00eb out 0xA1,al ;屏蔽从芯片所有中断请求。 ret empty_8042: ;只有当输入缓冲器为空时(状态寄存器位1 = 0) ;才可以对其执行写命令。 dw 0x00eb,0x00eb in al,0x64 ;读AT键盘控制器状态寄存器。 test al,2 ;测试位1,输入缓冲器满? jnz empty_8042 ;yes - loop ret idt_48: dw 0x800 ;这里不能像书上设置成0,否则VMWARE调试会出错! dw 0,0 ;IDT全部中断都设置成无效 gdt_48: dw 0x800 ;GDT长度设置为 2KB(0x7ff)表中共可有 256项。 dw 512+gdt,0x9 ;GDT物理地址:0x90200 + gdt gdt:dw 0,0,0,0 ;0#描述符,它是空描述符 dw 0x07FF ;8Mb - limit=2047 (2048*4096=8Mb) dw 0x0000 ;base address=0 dw 0x9A00 ;code read/exec 代码段为只读、可执行 dw 0x00C0 ;granularity=4096, 386 颗粒度为4096,32位模式 dw 0x07FF ;8Mb - limit=2047 (2048*4096=8Mb) dw 0x0000 ;base address=0 dw 0x9200 ;data read/write 数据段为可读可写 dw 0x00C0 ;granularity=4096, 386颗粒度为4096,32位模式 showmsg: call newline call printstr ret printstr: ;显示指定的字符串, 以'$'为结束标记 mov al,[si] cmp al,'$' je disover mov ah,0eh int 10h inc si jmp printstrdisover: ret newline: ;显示回车换行 mov ah,0eh mov al,0dh int 10h mov al,0ah int 10h ret welcome db '(ii) Welcome Linux---setup!',0x0d,0x0a,'$'msg1 db '1.Get memory size','$'msg2 db '2.Check for EGA/VGA and some config parameters','$'msg3 db '3.Get video-card data','$'msg4 db '4.Get hd0 data','$'msg5 db '5.Get hd1 data','$'msg6 db '6.Check that there IS a hd1','$'msg7 db '7.Move system from 0x10000 to 0x00000','$'msg8 db '8.Now Ready to Protect Mode!','$' [bits 32]inprotect: ;测试进入保护模式后是否正常 mov eax,2*8 ;加载数据段选择子(0x10)mov ds,eaxmov esi,sysmsg+SETUPSEG*0x10 ;保护模式DS=0,数据需跨过段基址用绝对地址访问 mov edi, 0xb8000+18*160 ;显示在第18行,显卡内存地址也需用绝对地址访问 call printnewmov esi,promsg+SETUPSEG*0x10mov edi, 0xb8000+20*160 ;显示在第20行call printnewmov esi,headmsg+SETUPSEG*0x10mov edi, 0xb8000+22*160 ;显示在第22行call printnewjmp $printnew: ;保护模式下显示字符串, 以'$'为结束标记 mov bl ,[ds:esi] cmp bl, '$' je printover mov byte [ds:edi],bl inc edi mov byte [ds:edi],0x0c ;字符红色 inc esi inc edi jmp printnewprintover: ret sysmsg db '(iii) Welcome Linux---system!','$'promsg db '1.Now Already in Protect Mode','$'headmsg db '2.Run head.asm in system program','$'times 512*4-($-$$) db 0 ;控制setup最终的机器代码长度为4个扇区 4.head.asm;*;*Linux操作系统Nasm引导程序:head,制作者:Mr.Jiang;2020-10-27*%include "config.inc"SETUPSEG equ DEF_SETUPSEG ;全部同bootsect和setup SYSSEG equ DEF_SYSSEG_pg_dir equ 0x0000 ;页目录地址,大小4KB. pg0 equ 0x1000 ;第1个页表地址,大小4KB. pg1 equ 0x2000 ;第2个页表地址,大小4KB.pg2 equ 0x3000 ;第3个页表地址,大小4KB.pg3 equ 0x4000 ;第4个页表地址,大小4KB._tmp_floppy_area equ 0x5000 ;软盘缓冲区地址. len_floppy_area equ 0x400 ;软盘缓冲区大小1KB [bits 32] ;指定代码为32位保护模式 jmp start;这条伪指令不会执行任何操作,只在编译的时候起填充数字作用。 times _tmp_floppy_area+len_floppy_area-($-$$) db 0 ;;一个语句实现页目录和页表地址区域清0,省去程序后面Linux源代码中的清0部分 ;使head程序从0x5000+0x400位置开始放置(仅除第一条jmp指令外)。 ;这里已经处于32位运行模式,首先设置ds,es,fs,gs为setup.s中构造的内核数据段;并将堆栈放置在stack_start指向的user_stack数组区,然后使用本程序后面定义的;新中断描述符表和全局段描述表。新全局段描述表中初始内容与setup.s中的基本一样,;仅段限长从8MB修改成了16MB。stack_start定义在kernel/sched.c。它指向user_stack;数组末端的一个长指针。设置这里使用的栈,姑且称为系统栈。但在移动到任务0执行;(init/main.c中137行)以后该栈就被用作任务0和任务1共同使用的用户栈了。start: mov eax,2*8 ;加载数据段选择子(0x10)mov ds,eax ;把所有数据类段寄存器全部指向GDT的数据段地址 mov es,eax mov fs,eaxmov gs,eaxmov ss,eaxmov esi,sysmsg ;保护模式DS=0,数据用绝对地址访问mov cl, 0x0c ;颜色红 mov edi, 0xb8000+13*160 ;显示在第18行,显卡内存地址也需用绝对地址访问call printnew mov esi,promsg mov cl, 0x0cmov edi, 0xb8000+15*160 ;显示在第20行call printnewmov esi,headmsg mov cl, 0x0cmov edi, 0xb8000+16*160 ;显示在第22行 call printnewmov esp,0x1e25c ; 重新设置堆栈,暂时设置值参见书 ;《Linux内核设计的艺术_图解Linux操作系统架构 ; 设计与实现原理》P27 ; Linus源程序中是lss _stack_start,%esp ; _stack_start,。定义在kernel/sched.c,82-87行 ; 它是指向 user_stack数组末端的一个长指针 call setup_idtcall setup_gdt jmp 1*8:newgdt ;改变CS的值来触发新GDT表生效 nop nopnewgdt: ;如能正常打印则表明程序正常运行,新GDT表无问题 mov esi,gdtmsg ;保护模式DS=0,数据用绝对地址访问mov cl, 0x09 ;颜色蓝mov edi, 0xb8000+17*160 ;显示在第18行,显卡内存地址也需用绝对地址访问call printnew ;call test_keyboard ;开键盘中断并按键测试,显示外部中断体系正常sti ;开中断int 00h ;手工系统中断调用,测试显示内部中断体系也正常 cli ;关掉中断 call A20openmov esi,a20msg ;保护模式DS=0,数据用绝对地址访问mov cl, 0x09 ;蓝色mov edi, 0xb8000+19*160 ;显示在第18行,显卡内存地址也需用绝对地址访问call printnew;前面3个入栈0值分别表示main函数的参数envp、argv指针和argc,但main()没有用到。;push _main入栈操作是模拟调用main时将返回地址入栈的操作,所以如果main.c程序;真的退出时,就会返回到这里的标号L6处继续执行下去,也即死循环。push _main将;main.c的地址压入堆栈。这样,在设置分页处理(setup_paging)结束后执行'ret';返回指令时就会将main.c;程序的地址弹出堆栈,并去执行main.c程序了。push 0 ;These are the parameters to main :-)push 0 ;这些是调用main程序的参数(指init/main.c)。push 0 push L6 ;return address for main, if it decides to.push _main ;'_main'是编译程序对main的内部表示方法。jmp setup_paging ;这里用的JMP而不是call,就是为了在setup_paging结束后的 ;ret指令能去执行C程序的main() L6:jmp L6 ;main程序绝对不应该返回到这里。不过为了以防万一, ;所以添加了该语句。这样我们就知道发生什么问题了。 _main: ;这里暂时模拟出C程序main() mov esi,mainmsg ;保护模式DS=0,数据用绝对地址访问 mov cl, 0x09 ;蓝色 mov edi, 0xb8000+22*160 ;指定显示在某行,显卡内存地址需用绝对地址 call printnew ;0xb8000为字符模式下显卡映射到的内存地址 ret test_keyboard: ; 测试键盘中断mov al, 11111101b ; 开启键盘中断开关 out 021h, al ; 主8259, OCW1.dw 0x00eb,0x00eb ;时延mov al, 11111111b ; 屏蔽从芯片所有中断请求out 0A1h, al ; 从8259, OCW1.dw 0x00eb,0x00eb ;时延ret;Linux将内核的内存页表直接放在页目录之后,使用了4个表来寻址16 MB的物理内存。;如果你有多于16 Mb的内存,就需要在这里进行扩充修改。;每个页表长为4KB(1页内存页面),而每个页表项需要4个字节,因此一个页表共可存;1024个表项。一个页表项寻址4KB的地址空间,则一个页表就可以寻址4MB的物理内存。setup_paging:;首先对5页内存(1页目录 + 4页页表)清零。由于在程序第一行已经实现,此处可省。 ;mov ecx,10;xor eax,eax;xor edi,edi ;页目录从0x000地址开始。 ;cld ;edi按递增方向 ;rep;stosd ;eax内容存到es:edi所指内存位置处,且edi增4。;下面4句设置页目录表中的项。因为内核共有4个页表,所以只需设置4项(索引)。;页目录项的结构与页表中项的结构一样,4个字节为1项。;例如"pg0+7"表示:0x00001007,是页目录表中的第1项。;则第1个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000;;第1个页表的属性标志 = 0x00001007&0x00000fff = 0x07,表示该页存在、用户可读写。;一句指令就把页表的地址和属性完全完整定义了,这个写法设计得有点巧妙。 mov dword [_pg_dir],pg0+7 ;页表0索引 将直接覆盖0地址处的3字节长度jmp指令 mov dword [_pg_dir+4],pg1+7 ;页表1索引mov dword [_pg_dir+8],pg2+7 ;页表2索引mov dword [_pg_dir+12],pg3+7 ;页表3索引 ;下面填写4个页表中所有项的内容,共有:4(页表)*1024(项/页表)=4096项(0-0xfff),;也即能映射物理内存 4096*4Kb = 16Mb。;每项的内容是:当前项所映射的物理内存地址 + 该页的标志(这里均为7)。;填写使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。;每一个页表中最后一项在表中的位置是1023*4 = 4092.;此最后一页的最后一项的位置就是pg3+4092。mov edi,pg3+4092;edi->最后一页的最后一项。mov eax,0xfff007;16Mb - 4096 + 7 (r/w user,p) */;最后1项对应物理内存页面的地址是0xfff000,;加上属性标志7,即为0xfff007。std ;方向位置位,edi值递减(4字节)。goon:stosd sub eax,0x1000;每填写好一项,物理地址值减0x1000。jge goon ;如果小于0则说明全添写好了。 jge是大于或等于转移指令;现在设置页目录表基址寄存器cr3,指向页目录表。cr3中保存的是页目录表的物理地址;再设置启动使用分页处理(cr0的PG标志,位31)xor eax,eax ;pg_dir is at 0x0000 */ # 页目录表在0x0000处。mov cr3,eax ;cr3 - page directory start */mov eax,cr0or eax,0x80000000 ;添上PG标志。mov cr0,eax ; set paging (PG) bit */软盘缓冲区: 共保留1024项,填充数值0。在程序第一行已经实现,此处可省。;mov ecx,1024/4; ;xor eax,eax;mov edi,_tmp_floppy_area ;软盘缓冲区从0x5000地址开始。;cld ;edi按递增方向;rep;stosd ;eax内容存到es:edi所指内存位置处,且edi增4。mov esi,pagemsg ;保护模式DS=0,数据用绝对地址访问mov cl, 0x09 ;蓝色字体 mov edi, 0xb8000+20*160 ;指定显示在某行,显卡内存地址也需用绝对地址访问call printnew mov esi,asmmsg ;保护模式DS=0,数据用绝对地址访问mov cl, 0x09 ;蓝色字体mov edi, 0xb8000+21*160 ;指定显示在某行,显卡内存地址也需用绝对地址访问 call printnewret ;setup_paging这里用的是返回指令ret。;该返回指令的另一个作用是将压入堆栈中的main程序的地址弹出,;并跳转到/init/main.c程序去运行。本程序到此就真正结束了。 ;用于测试A20地址线是否已经开启。采用的方法是向内存地址0x000000处写入任意;一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,;就一直比较下去,也即死循环表示地址A20线没有选通,就不能使用1MB以上内存。A20open: xor eax, eax inc eax mov [0x000000],eax cmp eax,[0x100000] je A20open ret printnew: ;保护模式下显示字符串, 以'$'为结束标记 mov bl ,[ds:esi] cmp bl, '$' je printover mov byte [ds:edi],bl inc edi mov byte [ds:edi],cl ;字符颜色 inc esi inc edi jmp printnewprintover: ret setup_idt: ;暂时将所有的中断全部指向一个中断服务程序:ignore_int lea edx,[ignore_int] ;将ignore_int的有效地址(偏移值)值送edx mov eax,0x00080000 ;将选择符0x0008置入eax的高16位中。 mov ax,dx ;selector = 0x0008 = cs */ ;偏移值的低16位置入eax的低16位中。此时eax含有门 ;描述符低4字节的值。 mov dx,0x8E00 ;interrupt gate - dpl=0, present ;此时edx含有门描述符高4字节值,偏移地址高16位是0 lea edi,[_idt] ;_idt是中断描述符表的地址。 ;以上为单独一个中断描述符的设置方法 mov ecx,256 ;IDT表中创建256个中断描述符 ;将上面的中断描述符重复放置256次,让所有的中断全部指向一个中断服务程序:哑中断 rp_sidt: mov [edi],eax ;将哑中断门描述符存入表中。 mov [edi+4],edx ;edx内容放到 edi+4 所指内存位置处。 add edi,8 ; edi指向表中下一项。 loop rp_sidt lidt [idt_descr] ;加载中断描述符表寄存器值。 ret ;让所有的256中断都指向这个统一的中断服务程序 ignore_int: cli ;首先应禁止中断,以免中断嵌套 pushad ;进入中断服务程序首先保存32位寄存器 push ds ;再保存所有的段寄存器 push es push fs push gs push ss mov eax,2*8 ;进入断服务程序后所有数据类段寄存器都转到内核段 mov ds,eax mov es,eax mov fs,eax mov gs,eax mov ss,eax mov esi,intmsg ;保护模式DS=0,数据用绝对地址访问 mov cl, 0x09 ;蓝色 mov edi, 0xb8000+18*160 ;指定显示在某行,显卡内存需用绝对地址 call printnew pop ss ;恢复所有的段寄存器 pop gs pop fs pop es pop ds popad ; 所有32位寄存器出栈恢复 iret ;中断服务返回指令 align 2 ;按4字节方式对齐内存地址边界。dw 0 ;这里先空出2字节,这样_idt长字是4字节对齐的。;下面是加载中断描述符表寄存器idtr的指令lidt要求的6字节操作数。;前2字节是idt表的限长,后4字节是idt表在线性地址空间中的32位基地址。idt_descr: dw 256*8-1 ;idt contains 256 entries # 共256项,限长=长度 - 1。 dd _idt ret setup_gdt: lgdt [gdt_descr] ;加载全局描述符表寄存器。 ret align 2 ;按4字节方式对齐内存地址边界。dw 0 ;这里先空出2字节,这样_gdt长字是4字节对齐的。;加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数。前2字节是gdt表的限长,;后4字节是gdt表的线性基地址。因为每8字节组成一个描述符项,所以表中共可有256项。;符号_gdt是全局表在本程序中的偏移位置。gdt_descr: dw 256*8-1 dd _gdt sysmsg db '(iii) Welcome Linux---system!','$'promsg db '1.Now Already in Protect Mode','$'headmsg db '2.Run head.asm in system program','$'gdtmsg db '3.Reset GDT success:New CS\EIP normal','$'intmsg db '4.Reset IDT success:Unknown interrupt','$' a20msg db '5.Check A20 Address Line Stdate:Open','$'pagemsg db '6.Memory Page Store:Page Tables is set up','$'asmmsg db '7.Pure Asm Program:bootsect->setup->head(system) is Finished','$'mainmsg db '8.Now Come to C program entry:Main()','$';IDT表和GDT表放在程序head的最末尾;中断描述符表:256个,全部初始化为0。 _idt: times 256 dq 0 ;idt is uninitialized # 256项,每项8字节,填0。;全局描述符表。其前4项分别是:空项、代码段、数据段、系统调用段描述符,;后面还预留了252项的空间,用于放置新创建任务的局部描述符(LDT)和对应的;任务状态段TSS的描述符。;(0-nul,1-cs,2-ds,3-syscall,4-TSS0,5-LDT0,6-TSS1,7-LDT1,8-TSS2 etc...) _gdt: dq 0x0000000000000000 ;NULL descriptor */ dq 0x00c09a0000000fff ;16Mb */ # 0x08,内核代码段最大长度16MB。 dq 0x00c0920000000fff ;16Mb */ # 0x10,内核数据段最大长度16MB。 dq 0x0000000000000000 ;TEMPORARY - don't use */ times 252 dq 0 ;space for LDT's and TSS's etc */ # 预留空间。
2026年01月04日
3 阅读
0 评论
0 点赞
2025-09-21
C语言枚举end是做什么用的?
最近在知乎上看到一个问题:C语言枚举end是做什么用的?刚开始,我也有一些疑惑,后面查了一些资料,对于这个问题,简单地说一下我的看法。枚举有多大?枚举类型到底有多大,占多少空间呢?这个要具体情况具体分析,编译器会视情况而定。下面是我测试用的编译器版本:gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0Copyright (C) 2017 Free Software Foundation, Inc.This is free software; see the source for copying conditions. There is NOwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.当我写下这段代码的时候,实际的输出会是多少呢?有人会说是 1,有人会说是 4,我最终运行的确实是4;▲输出结果但是,这个结果并不是唯一的,它取决于你的编译器,另外还取决于编译器参数,gcc这里有个编译器参数 -fshort-enums,如果我们在编译的时候加上这个,那么编译出来是什么呢?▲短枚举的输出结果最终结果变成了1现在我在原先的代码中,加入CMD_MAX_16BIT = 0xFFFF,下面看看输出结果是多少。▲增带值范围运行输出结果如下:▲输出结果是的,它变成了2。因此,我们可以得出结论就是:编译器将为枚举分配足够的内存大小,来保存我们所声明的任何值。所以,如果我们的代码中只使用低于 256(8位的范围是0~255) 的值,我们的枚举应该是 8 位宽,也就是一个字节,而后面的0xFFFF显然是16位,两个字节,所以最终输出为2为此,我参考了一下gcc user manual,如下;https ://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html-fshort-enumsAllocate to an enum type only as many bytes as it needs for the declared range of possible values. Specifically, the enum type is equivalent to the smallest integer type that has enough room.Warning: the -fshort-enums switch causes GCC to generate code that is not binary compatible with code generated without that switch. Use it to conform to a non-default application binary interface.所以,我们需要明确的是编译器是否会默认执行 -fshort-enums这个命令,大多数是不会的,这里我还测试了一些clang,具体结果和gcc相同。但是,在嵌入式编程中需要注意,这里我查了一下,IAR的编译器默认会执行 -fshort-enums 。电脑上没有IAR,这里我参考了IAR 的 ARM C 编译器的文档IAR C/C++ Development Guide。可以看到enum类型默认的规定,如果要强制为int类型的话,需要编译的时候提那就--enum_is_int的编译参数,如下所示:▲枚举类型所以,这里为了避免编译器的优化,以及不同的硬件平台和不同编译器,从而导致枚举分配内存空间的变化,所以上述增加了一个0xFFFFFFFF,强制编译器为枚举分配4个字节的空间。▲设置最大范围为4字节最终的输出结果都是4,如下图所示:▲输出结果比较看来虽然是一个很小的知识点,但这中间的坑还真不少!好了,本期的文章就到这里了,我们下期再见。
2025年09月21日
1 阅读
0 评论
0 点赞
1
2
...
7