首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
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设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
搜索到
378
篇与
的结果
2026-01-07
Linux下搭建STM32开发环境
方案STM32开发一般使用官方的固件库,将MDK作为集成开发环境。MDK将代码编辑和工程构建全包了,这固然很方便,但是代码编辑功能较弱,而且MDK还是收费软件。于是便萌生了使用其他开发环境的想法。果然,有免费的高质量方案:GNU Toolchain + STM32CubeMX + VSCode:GNU Toolchain:包括编译器、链接器、调试器和C语言标准库在内的一整套工具链STM32CubeMX:图形化的软件配置工具,用来生成STM32初始化代码工程。ST官方已经停止更新固件库,STM32CubeMX是推荐的开发方式VSCode:牛逼哄哄的代码编辑器,搭配强大的插件,从此忘记Vim和Emacs上述的三个工具都是跨平台的,在Linux和Windows下都可以搭建,在Linux下搭建更方便。本文以Manjaro Linux为例,其他Linux发行版和Windows的搭建方法类似。GNU ToolchainGNU Arm Embedded Toolchain是目标平台为ARM嵌入式处理器的工具链软件。一般要安装编译器、调试器和函数库三个组件:sudo pacman -S arm-none-eabi-gcc arm-none-eabi-gdb arm-none-eabi-newlibarm-none-eabi-newlib是嵌入式平台下的C语言标准函数库,如果不安装可能会产生编译错误。Windows用户可以通过下载软件包来安装这套工具链,安装完后还需配置环境变量。STM32CubeMXST官方的固件库已经停止更新,官方推荐用STM32CubeMX生成项目的初始化代码。STM32CubeMX是图形化界面,点击鼠标就能配置,比固件库方便很多。STM32CubeMX依赖Java运行环境,要安装JRE 1.8:sudo pacman -S jdk8-openjdk在ST的官方网站上下载安装文件,我下载的是5.3.0版本。解压后有三个文件,Windows通过运行exe文件安装,Linux通过运行.linux后缀的文件安装:sudo ./SetupSTM32CubeMX-5.3.0.linux会跳出一个图形化安装界面,不停地点下一步就行。安装完成后,启动软件,点击File - New Project新建一个工程。此时会看到选择芯片型号的界面,可以通过勾选筛选条件过滤出目标芯片,我的芯片型号是STM32F103RCT6,ARM Cortex-M3架构。芯片型号选择好之后,点击Start Project,会有配置引脚的界面。在我的开发板上,PA8和PD2两个GPIO口驱动两个LED灯,点击对应的引脚,将它们配置为GPIO_Output。 GPIO配置 接下来是时钟配置,暂时先保持默认的配置吧。 时钟配置 再接下来是项目配置,比较重要是的Toolchain/IDE那一项的选择。我选择的是Makefile,使用make来管理项目。点击右上方的GENERATE CODE按钮生成项目的代码。Windows用户还可以选择MDK-ARM,最后会生成代码以及MDK的项目文件,可以用MDK打开、编辑和构建工程。 项目配置 这里再插一句,上图最下方可以看到有个Firmware目录的设置,默认配置在用户的home目录下。如果嫌碍眼,可以打开Help - Updater Settings,修改成其它的目录。工程的目录下生成了如下文件:$ ls -FDrivers/ Inc/ LedTest.ioc Makefile Src/ startup_stm32f103xe.s STM32F103RCTx_FLASH.ld可以看到有个Makefile文件;startup_stm32f103xe.s是芯片的启动代码,是汇编语言;Drivers/文件夹中是自动生成的芯片底层代码,和固件库的组织很类似;Src/目录下有包括main.c在内的需要用户修改的源码文件。VSCode如果用VSCode做C/C++开发,推荐安装C/C++ Intelligence这个插件。用VSCode打开项目文件夹和Src/目录下的main.c文件,并在main函数的while循环中加入控制流水灯的代码:生成的代码都以HAL开头,函数名和固件库中的也很类似。可以看到,STM32CubeMX在main函数中自动添加了初始化的代码。打开MX_GPIO_Init函数还能看到将PA8和PD2两个GPIO口设置为输出的配置代码。上图中有些宏定义下方有波浪线,将鼠标移动到附近还会提示「标识符未定义」。这时候按F1键,搜索Edit Configuration,打开json配置文件,添加一些信息帮助插件做代码检查和自动补全的工作。将Makefile中C_INCLUDES和C_DEFS的内容添加到includePath和defines中。编译器的头文件目录也要添加到includePath中。修改后的配置文件如下:{ "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**", "Inc", "Drivers/STM32F1xx_HAL_Driver/Inc", "Drivers/STM32F1xx_HAL_Driver/Inc/Legacy", "Drivers/CMSIS/Device/ST/STM32F1xx/Include", "Drivers/CMSIS/Include", "/usr/arm-none-eabi/include", "/usr/arm-none-eabi/include/c++/9.1.0", "/usr/arm-none-eabi/include/c++/9.1.0/backward", "/usr/lib/gcc/arm-none-eabi/9.1.0/include", "/usr/lib/gcc/arm-none-eabi/9.1.0/include-fixed" ], "defines": [ "USE_HAL_DRIVER", "STM32F103xE" ], "compilerPath": "/usr/bin/arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb", "cStandard": "c99", "cppStandard": "c++11", "intelliSenseMode": "clang-x64" } ], "version": 4 } 现在就可以构建工程了,在项目的根目录下直接运行make命令,编译链接完成后,工程根目录下会多出一个build/目录,里面有我们需要的LedTest.bin文件。stm32flash最后将二进制文件烧写到芯片中。一般来说,用JLink是最方便的,不仅可以烧写软件还能够进行硬件调试。我没有JLink,手头只有一根可以供电和烧写软件的USB转串口线,所以我介绍另一款名为stm32flash的烧写软件。stm32flash需要自己下载源码后编译安装:git clone https://github.com/stm32duino/stm32flashcd stm32flash/makesudo make install用跳线帽将板子的BOOT0引脚接3.3V,BOOT1引脚接地,用USB转串口线将板子和电脑连接,然后上电。我的Linux系统默认就有CH340芯片的驱动,上电后多了一个名为/dev/ttyUSB0的设备。可以用stm32flash查看串口的信息:$ sudo stm32flash /dev/ttyUSB0一切正常,然后用下面的命令将编译生成的二进制文件烧写入芯片中:$ sudo stm32flash -w LedTest.bin -v -g 0x00 /dev/ttyUSB0烧写完后就可以在板子上看到LED闪烁的效果。该软件更多的功能可以通过运行stm32flash -h命令查看。参考使用VSCode打造STM32开发环境https://panqiincs.me/2019/07/26/develop-stm32-on-linux/
2026年01月07日
9 阅读
0 评论
0 点赞
2026-01-07
Win10环境下VMware安装Mac OS虚拟机并在虚拟机中安装xcode
环境及版本首先本次安装是基于VMware workstations 15 player,安装的是Mac OS 10.15版本(这个版本安装之后会提示升级),xcode根据apple store最新版本安装一.准备工作对于初次安装的人来说简直就是噩梦,我自己就浪费了一天的时间来解决各种问题,所以大半夜的写一篇文章,让读者少浪费时间。1.下载VMware workstation 15 player 、Mac OS10.15镜像、unlocker-master解锁工具以及全屏补丁darwin(版本都是对应的,建议都用我提供的)链接:百度网盘 (https://pan.baidu.com/s/1nD5Bs9GmnyZIBWajSADNSQ)提取码:56792.注册Apple ID注册地址:https://appleid.apple.com/account#!&page=create二.步骤1.完全关闭VMware,打开过就需要结束进程(必须)桌面右键“我的电脑→管理→服务和应用程序→服务”,下滑可以看到这四个服务(不同版本的VMware可能这几个服务的名字会有差异但都差不多,总之有几个与VMware相关的都关闭就行)将其全部关闭。2.打开打开unlock-master文件夹,找到win-install.cmd并右键以管理员身份运行,注意不以管理员身份运行会一闪而过且没有作用,所以必须是右键以管理员身份运行这里可能还会有下载,等待下载完再进行剩下的操作,下载完之后会自动关闭。3.打开Vmware新建虚拟机选择准备好的Mac OS catalina 10.15.0镜像(由于不是iso文件,需要选择所有文件才会显示)4.点击下一步选择 apple Mac OS X(M) (没有右键以管理员身份运行win-install.cmd是不会出现此选项的),并选择macOS 10.15版本。5.其他安装步骤选择位置(自己选择)分配磁盘,这里建议100G左右(虚拟的),40G后面安装xcode会提示磁盘容量不够。自定义硬件:内存建议4GB ;处理器建议4核(自己电脑8核也可以选择8核,可以在任务管理器性能选项中查看自己电脑的逻辑处理器的核数);网络适配器:桥接模式最后的配置图:6.【运行虚拟机,下面这种情况我没有遇到(没出错可以跳过),但是还是把网上的案例分享出来,供参考:】首次运行时,很小的可能会出现错误提示(如图)。这时不要着急找到在之前的创建新的虚拟机时,设置的虚拟机位置。打开该目录,找到macOSxx.xx.vmx文件(我这里的文件是:macOS 10.15.vmx),右键用记事本方式打开,找到 smc.present = “TRUE”在smc.present = “TRUE”下面,手动添加一行smc.version= "0"然后保存关闭,再重新启动虚拟机,就不会报错了。当你遇到“VMware 锁定文件失败,开启模块 disk 的操作失败, 未能启动虚拟机”情况时。可以直接找到你的虚拟机所在目录(不是VMware的安装目录),找到虚拟机系统文件目录,进入到文件夹下。删除以.lck为后缀名的文件(篮框内输入.lck)重新启动7.开始安装选择语言继续选择磁盘工具继续打开磁盘工具后,选中“VMwareVirtual SATA Hard Drive Media”点上方的“抹掉”,并重新命名(我这里随便命名为xixue),然后点抹掉。之后点击完成,再点击关闭关闭后回到了上一级的菜单,然后选择安装Mac OS,点击继续。一路点击继续和同意,直到这一步,点击刚创建的磁盘,再点击安装,然后等待安装。等待许久之后,还有以下简单的操作选择中国大陆点击继续后面的都直接点击继续这一步就要用到Apple ID登录之后同意条款,之后创建用户,后面的几个页面都可以直接点击继续最后选取完外观之后等待安装8.等待一会后进入桌面,会跳出更新,是系统需要更新建议点击安装,下载更新,不然安装xcode时会提示版本需要更新。9.安装好系统之后,点击苹果标志,再点击关于本机,可以看到显示器只有3M,所以最大化虚拟机时会出现分辨率不正常,需要安装补丁来解决重启虚拟机,并按住Command(即对应Windows键)+ R键(可能一次成功不了);11.然后到如下界面,点击实用工具→终端。12.打开终端后,输入 csrutil disable 后回车执行成功后,返回信息如下:Successfully disabled System IntegrityProtection. Please restart the machine for the changes to take effect.之后输入 reboot 重启。12.、重启后安装darwin,先推出系统中的光驱(推出就相当于windows系统U盘安装好了以后拔掉)13.在虚拟机中的macos10.15选项卡中右键打开CD/DVD(SATA) → 设置14.把macos虚拟机中的光驱ios映像文件改成准备好的darwin15.、在虚拟机中的macos10.15选项卡中右键打开CD/DVD(SATA) → 连接16.桌面会出现一个名为“VMware tools”光驱图标17.双击安装VMware Tools打开点击继续18.默认路径安装19.安装完成后,点击重启20.重启之后选择全屏模式就可以全屏显示了21.在Apple store搜索xcode进行获取安装(我这里是已经安装了)有可能会提示Apple ID不能安装,点击检查,绑定支付宝或者银行卡完善信息就可以了22.OK大功告成,下面尝试用xcode做一个简单的项目打开xcode,选择create a new xcode project如图选择,点击next第一个为app的名字,注意选择语言为objective-c选择文件存放路径,就放在桌面,或者可以自己创建文件夹出现这个提示框直接cancel就行这里可以选择在什么Apple手机上运行依次这里可以选择在什么Apple手机上创作我们来做一个简单的hello world项目可以给主界面添加一张背景图,依次点击工具搜索找到UiImageView拖到主界面,缩放大小跟手机屏幕一样大然后点击Assets.xcassets,在空白处右键,选择import导入图片,点击open回到Main.storyboard,依次点击手机屏幕,点击Image,就可以看到刚才导入的图片,点击选择即可成为背景。再依次点击工具栏,拖入一个label,设置其text属性为Hello world,以及颜色字号点击运行等待虚拟机出来就可以看到了(这里图片分辨率有点不合适)ok,完成了,说明xcode可以正常使用。三.其他优化1.时间默认时间可能是美国的,需要换的可以看看下面的步骤点击打开日期与时间偏好设置点击时区,再点击这个小锁,选择时区,时间就改过来了,再锁定。2.窗口缩放可能有人不喜欢默认的窗口缩放样式,可以看看下面的步骤更改依次点击“系统偏好设置→程序坞”,选择缩放效果就好了我也是初学者,初次安装遇到了很多麻烦,所以整理了一下,有什么其他问题也可以留言谈论一下。————————————————版权声明:本文为CSDN博主「FitzSimmons79」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_44272593/article/details/105649040
2026年01月07日
30 阅读
0 评论
0 点赞
2026-01-06
spi master接口的fpga实现
https://www.cnblogs.com/kingstacker/p/7490002.html前言当你器件的引脚贼少的时候,需要主机和从机通信,spi就派上了用场,它可以一对多,但只是片选到的从机能和主机通信,其他的挂机。spi:serial peripheral interface 串行外围接口大致了解:spi是个同步协议,数据在master和slaver间交换通过时钟sck,由于它是同步协议,时钟速率就可以各种变换。sck:主机提供,从机不能操控,从器件由主机产生的时钟控制。数据只有在sck来了的上升沿或者下降沿才传输。高级一点的spi芯片有配置寄存器,高级一点的工作有四种模式,采样相位和sck空闲电平可配置。当然在这里我们主要实现简单的spi协议:sck是系统时钟的四分频,wr请求信号有效时,主机开始工作,数据位8bit,sck空闲时低电平,工作时第一个沿数据传输。只有一个从机,cs低电平片选。看下结构:接口定义:编码实现:(版权所有,请勿用于商业用途,仅供学习使用)//************************************************ // Filename : spi_ms_test1.v // Author : Kingstacker // Company : School // Email : kingstacker_work@163.com // Device : Altera cyclone4 ep4ce6f17c8 // Description : spi master module;data 8bit;sck is 4 div of the clk; //************************************************ module spi_ms #(parameter WIDTH = 8)( //input; input wire clk, input wire rst_n, input wire wr, //send request; input wire [WIDTH-1:0] master_din, //the data you want send; input wire miso, //the data form slave; //output; output reg cs, //slave select; output reg sck, //data exchange clock; output reg mosi, //master out; output reg [WIDTH-1:0] master_dout //the data you received; ); localparam CLK_HZ = 50_000_000; //clk frequency; localparam SCK_HZ = 12_500_000; //sck frequency; localparam DIV_NUMBER = CLK_HZ / SCK_HZ; localparam CNT_MAX = (DIV_NUMBER >>1) - 1'b1; localparam DATA_CNT_MAX = 5'd31; localparam MOSI_CNT_MAX = 3'd7; localparam IDEL = 2'b00; localparam SEND = 2'b01; localparam FINISH = 2'b10; reg cnt; //sck cnt; reg sck_en; //enable sck; reg data_cnt_en; reg sck_reg1; reg sck_reg2; wire sck_p; //posedge sck; wire sck_n; //negedge sck; wire send_over; reg [1:0] cstate; reg [4:0] data_cnt; //cnt the send data; reg [2:0] mosi_cnt; reg [WIDTH-1:0] master_din_reg; reg [WIDTH-1:0] master_dout_reg; //produce sck; always @(posedge clk or negedge rst_n) begin if (~rst_n) begin cnt <= 0; sck <= 1'b0; end //if else begin if (sck_en == 1'b1) begin if (cnt == CNT_MAX) begin cnt <= 0; sck <= ~sck; end else begin cnt <= cnt + 1'b1; sck <= sck; end end else begin cnt <= 0; sck <= 1'b0; end end //else end //always //produce sck_p and sck_n; always @(posedge clk or negedge rst_n) begin if (~rst_n) begin sck_reg1 <= 1'b0; sck_reg2 <= 1'b0; end //if else begin sck_reg1 <= sck; sck_reg2 <= sck_reg1; end //else end //always assign sck_p = (sck_reg1 & (~sck_reg2)); //sck posedge; assign sck_n = ((~sck_reg1) & sck_reg2); //sck negedge; //fsm;hot code; always @(posedge clk or negedge rst_n) begin if (~rst_n) begin cstate <= IDEL; end else begin case (cstate) IDEL: cstate <= (wr)? SEND : IDEL; SEND: cstate <= (send_over) ? FINISH : SEND; FINISH: cstate <= IDEL; default: cstate <= IDEL; endcase //case end end always @(posedge clk or negedge rst_n) begin if (~rst_n) begin cs <= 1'b1; data_cnt_en <= 1'b0; sck_en <= 1'b0; master_din_reg <= 0; master_dout <= 0; end else begin case (cstate) IDEL: begin data_cnt_en <= 1'b0; master_din_reg <= (wr) ? master_din : master_din_reg; //load the data you want send to slaver; end SEND: begin data_cnt_en <= 1'b1; cs <= 1'b0; sck_en <= 1'b1; master_dout <= (send_over) ? master_dout_reg : master_dout; //master receiverd data; end FINISH: begin //send and load ok; sck_en <= 1'b0; cs <= 1'b1; data_cnt_en <= 1'b0; end default: begin cs <= 1'b1; sck_en <= 1'b0; data_cnt_en <= 1'b0; end endcase //case end end always @(posedge clk or negedge rst_n) begin if (~rst_n) begin data_cnt <= 0; end else begin data_cnt <= (data_cnt_en) ? (data_cnt + 1'b1) : 5'd0; //4 div * 8bit = 32 cnt; end end assign send_over = (data_cnt == DATA_CNT_MAX) ? 1'b1 : 1'b0; //rising edge miso; always @(posedge clk or negedge rst_n) begin if (~rst_n) begin master_dout_reg <= 0; end else begin master_dout_reg <= (sck_p) ? {master_dout_reg[6:0],miso} : master_dout_reg; end end //mosi; always @(posedge clk or negedge rst_n) begin if (~rst_n) begin mosi_cnt <= 0; end else begin if (sck_n) begin if (mosi_cnt == MOSI_CNT_MAX) begin mosi_cnt <= 0; end else begin mosi_cnt <= mosi_cnt + 1'b1; end end else begin mosi_cnt <= mosi_cnt; end end end always @(posedge clk or negedge rst_n) begin if (~rst_n) begin mosi <= 1'b0; end else begin mosi <= (sck_n) ? master_din_reg[MOSI_CNT_MAX-mosi_cnt] : mosi; end end endmodule仿真:综合资源使用:Fmax:
2026年01月06日
1 阅读
0 评论
0 点赞
2026-01-06
形象易懂讲解算法I——小波变换
https://zhuanlan.zhihu.com/p/22450818从傅里叶变换到小波变换,并不是一个完全抽象的东西,可以讲得很形象。小波变换有着明确的物理意义,如果我们从它的提出时所面对的问题看起,可以整理出非常清晰的思路。下面我就按照傅里叶-->短时傅里叶变换-->小波变换的顺序,讲一下为什么会出现小波这个东西、小波究竟是怎样的思路。(反正题主要求的是通俗形象,没说简短,希望不会太长不看。。)一、傅里叶变换关于傅里叶变换的基本概念在此我就不再赘述了,默认大家现在正处在理解了傅里叶但还没理解小波的道路上。(在第三节小波变换的地方我会再形象地讲一下傅里叶变换)下面我们主要讲傅里叶变换的不足。即我们知道傅里叶变化可以分析信号的频谱,那么为什么还要提出小波变换?答案在于,“对非平稳过程,傅里叶变换有局限性”。看如下一个简单的信号:做完FFT(快速傅里叶变换)后,可以在频谱上看到清晰的四条线,信号包含四个频率成分。一切没有问题。但是,如果是频率随着时间变化的非平稳信号呢?如上图,最上边的是频率始终不变的平稳信号。而下边两个则是频率随着时间改变的非平稳信号,它们同样包含和最上信号相同频率的四个成分。做FFT后,我们发现这三个时域上有巨大差异的信号,频谱(幅值谱)却非常一致。尤其是下边两个非平稳信号,我们从频谱上无法区分它们,因为它们包含的四个频率的信号的成分确实是一样的,只是出现的先后顺序不同。可见,傅里叶变换处理非平稳信号有天生缺陷。它只能获取一段信号总体上包含哪些频率的成分,但是对各成分出现的时刻并无所知。因此时域相差很大的两个信号,可能频谱图一样。然而平稳信号大多是人为制造出来的,自然界的大量信号几乎都是非平稳的,所以在比如生物医学信号分析等领域的论文中,基本看不到单纯傅里叶变换这样naive的方法。上图所示的是一个正常人的事件相关电位。对于这样的非平稳信号,只知道包含哪些频率成分是不够的,我们还想知道各个成分出现的时间。知道信号频率随时间变化的情况,各个时刻的瞬时频率及其幅值——这也就是时频分析。二、短时傅里叶变换(Short-time Fourier Transform, STFT)一个简单可行的方法就是——加窗。我又要套用方沁园同学的描述了,“把整个时域过程分解成无数个等长的小过程,每个小过程近似平稳,再傅里叶变换,就知道在哪个时间点上出现了什么频率了。”这就是短时傅里叶变换。看图:时域上分成一段一段做FFT,不就知道频率成分随着时间的变化情况了吗!用这样的方法,可以得到一个信号的时频图了:——此图像来源于“THE WAVELET TUTORIAL”图上既能看到10Hz, 25 Hz, 50 Hz, 100 Hz四个频域成分,还能看到出现的时间。两排峰是对称的,所以大家只用看一排就行了。是不是棒棒的?时频分析结果到手。但是STFT依然有缺陷。使用STFT存在一个问题,我们应该用多宽的窗函数?窗太宽太窄都有问题:窗太窄,窗内的信号太短,会导致频率分析不够精准,频率分辨率差。窗太宽,时域上又不够精细,时间分辨率低。(这里插一句,这个道理可以用海森堡不确定性原理来解释。类似于我们不能同时获取一个粒子的动量和位置,我们也不能同时获取信号绝对精准的时刻和频率。这也是一对不可兼得的矛盾体。我们不知道在某个瞬间哪个频率分量存在,我们知道的只能是在一个时间段内某个频带的分量存在。 所以绝对意义的瞬时频率是不存在的。)看看实例效果吧:——此图像来源于“THE WAVELET TUTORIAL”上图对同一个信号(4个频率成分)采用不同宽度的窗做STFT,结果如右图。用窄窗,时频图在时间轴上分辨率很高,几个峰基本成矩形,而用宽窗则变成了绵延的矮山。但是频率轴上,窄窗明显不如下边两个宽窗精确。所以窄窗口时间分辨率高、频率分辨率低,宽窗口时间分辨率低、频率分辨率高。对于时变的非稳态信号,高频适合小窗口,低频适合大窗口。然而STFT的窗口是固定的,在一次STFT中宽度不会变化,所以STFT还是无法满足非稳态信号变化的频率的需求。三、小波变换那么你可能会想到,让窗口大小变起来,多做几次STFT不就可以了吗?!没错,小波变换就有着这样的思路。但事实上小波并不是这么做的(关于这一点,方沁园同学的表述“小波变换就是根据算法,加不等长的窗,对每一小部分进行傅里叶变换”就不准确了。小波变换并没有采用窗的思想,更没有做傅里叶变换。)至于为什么不采用可变窗的STFT呢,我认为是因为这样做冗余会太严重,STFT做不到正交化,这也是它的一大缺陷。于是小波变换的出发点和STFT还是不同的。STFT是给信号加窗,分段做FFT;而小波直接把傅里叶变换的基给换了——将无限长的三角函数基换成了有限长的会衰减的小波基。这样不仅能够获取频率,还可以定位到时间了~【解释】来我们再回顾一下傅里叶变换吧,没弄清傅里叶变换为什么能得到信号各个频率成分的同学也可以再借我的图理解一下。傅里叶变换把无限长的三角函数作为基函数:这个基函数会伸缩、会平移(其实本质并非平移,而是两个正交基的分解)。缩得窄,对应高频;伸得宽,对应低频。然后这个基函数不断和信号做相乘。某一个尺度(宽窄)下乘出来的结果,就可以理解成信号所包含的当前尺度对应频率成分有多少。于是,基函数会在某些尺度下,与信号相乘得到一个很大的值,因为此时二者有一种重合关系。那么我们就知道信号包含该频率的成分的多少。仔细体会可以发现,这一步其实是在计算信号和三角函数的相关性。看,这两种尺度能乘出一个大的值(相关度高),所以信号包含较多的这两个频率成分,在频谱上这两个频率会出现两个峰。以上,就是粗浅意义上傅里叶变换的原理。如前边所说,小波做的改变就在于,将无限长的三角函数基换成了有限长的会衰减的小波基。这就是为什么它叫“小波”,因为是很小的一个波嘛~从公式可以看出,不同于傅里叶变换,变量只有频率ω,小波变换有两个变量:尺度a(scale)和平移量 τ(translation)。尺度a控制小波函数的伸缩,平移量 τ控制小波函数的平移。尺度就对应于频率(反比),平移量 τ就对应于时间。当伸缩、平移到这么一种重合情况时,也会相乘得到一个大的值。这时候和傅里叶变换不同的是,这不仅可以知道信号有这样频率的成分,而且知道它在时域上存在的具体位置。而当我们在每个尺度下都平移着和信号乘过一遍后,我们就知道信号在每个位置都包含哪些频率成分。看到了吗?有了小波,我们从此再也不害怕非稳定信号啦!从此可以做时频分析啦!做傅里叶变换只能得到一个频谱,做小波变换却可以得到一个时频谱!↑:时域信号↑:傅里叶变换结果——此图像来源于“THE WAVELET TUTORIAL”↑:小波变换结果小波还有一些好处,比如,我们知道对于突变信号,傅里叶变换存在吉布斯效应,我们用无限长的三角函数怎么也拟合不好突变信号:然而衰减的小波就不一样了:以上,就是小波的意义。以上只是用形象地给大家展示了一下小波的思想,希望能对大家的入门带来一些帮助。毕竟如果对小波一无所知,直接去看那些堆砌公式、照搬论文语言的教材,一定会痛苦不堪。在这里推荐几篇入门读物,都是以感性介绍为主,易懂但并不深入,对大家初步理解小波会很有帮助。文中有的思路和图也选自于其中:THE WAVELET TUTORIAL (强烈推荐,点击链接:Ihttp://users.rowan.edu/~polikar/WTtutorial.html)WAVELETS:SEEING THE FOREST AND THE TREESA Really Friendly Guide to WaveletsConceptual wavelets但是真正理解透小波变换,这些还差得很远。比如你至少还要知道有一个“尺度函数”的存在,它是构造“小波函数”的关键,并且是它和小波函数一起才构成了小波多分辨率分析,理解了它才有可能利用小波做一些数字信号处理;你还要理解离散小波变换、正交小波变换、二维小波变换、小波包……这些内容国内教材上讲得也很糟糕,大家就一点一点啃吧~一些问题的回答:关于海森堡不确定性原理不确定性原理,或者叫测不准原理,最早出自量子力学,意为在微观世界,粒子的位置与动量不可同时被确定。但是这个原理并不局限于量子力学,有很多物理量都有这样的特征,比如能量和时间、角动量和角度。体现在信号领域就是时域和频域。不过更准确一点的表述应该是:一个信号不能在时空域和频域上同时过于集中;一个函数时域越“窄”,它经傅里叶变换的频域后就越“宽”。如果有兴趣深入研究一下的话,这个原理其实非常耐人寻味。信号处理中的一些新理论在根本上也和它有所相连,比如压缩感知。如果你剥开它复杂的数学描述,最后会发现它在本质上能实现其实和不确定性原理密切相关。而且大家不觉得这样一些矛盾的东西在哲学意义上也很奇妙吗?关于正交化什么是正交化?为什么说小波能实现正交化是优势?简单说,如果采用正交基,变换域系数会没有冗余信息,变换前后的信号能量相等,等于是用最少的数据表达最大的信息量,利于数值压缩等领域。JPEG2000压缩就是用正交小波变换。比如典型的正交基:二维笛卡尔坐标系的(1,0)、(0,1),用它们表达一个信号显然非常高效,计算简单。而如果用三个互成120°的向量表达,则会有信息冗余,有重复表达。但是并不意味着正交一定优于不正交。比如如果是做图像增强,有时候反而希望能有一些冗余信息,更利于对噪声的抑制和对某些特征的增强。关于瞬时频率 原问题:图中时刻点对应一频率值,一个时刻点只有一个信号值,又怎么能得到他的频率呢? 很好的问题。如文中所说,绝对意义的瞬时频率其实是不存在的。单看一个时刻点的一个信号值,当然得不到它的频率。我们只不过是用很短的一段信号的频率作为该时刻的频率,所以我们得到的只是时间分辨率有限的近似分析结果。这一想法在STFT上体现得很明显。小波用衰减的基函数去测定信号的瞬时频率,思想也类似。(不过到了Hilbert变换,思路就不一样了,以后有机会细讲)关于小波变换的不足这要看和谁比了。A.作为图像处理方法,和多尺度几何分析方法(超小波)比:对于图像这种二维信号的话,二维小波变换只能沿2个方向进行,对图像中点的信息表达还可以,但是对线就比较差。而图像中最重要的信息恰是那些边缘线,这时候ridgelet(脊波), curvelet(曲波)等多尺度几何分析方法就更有优势了。B. 作为时频分析方法,和希尔伯特-黄变换(HHT)比:相比于HHT等时频分析方法,小波依然没脱离海森堡测不准原理的束缚,某种尺度下,不能在时间和频率上同时具有很高的精度;以及小波是非适应性的,基函数选定了就不改了。关于文中表述的严谨性评论中有不少朋友提到,我的一些表述不够精准。这是肯定的,并且我也是知道的。比如傅里叶变换的理解部分,我所说的那种“乘出一个大的值”的表述肯定是不够严谨的。具体我也在评论的回答中做了解释。我想说的是通俗易懂和精确严谨实在难以兼得,如果要追求严谨,最好的就是教科书上的数学表达,它们无懈可击,但是对于初学者来说,恐怕存在门槛。如果要通俗解释,必然只能侧重一个关键点,而出现漏洞。我想这也是教科书从来不把这些通俗解释写出来的原因吧——作者们不是不懂,而是怕写错。所以想深入理解傅里叶变换和小波变换的朋友还请认真学习教材,如果这篇文章能给一些初学者一点点帮助,我就心满意足了。
2026年01月06日
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 点赞
1
...
3
4
5
...
76