《操作系统真象还原》读书笔记 第3章

0x1 地址、section、vstart

0x1.1 什么是地址

地址只是数字,描述各种符号在源程序中的位置,它是源代码文件中各符号偏移文件开头的距离。由于指令和变量所占内存大小不同,故他们相对于文件开头偏移量参差不齐。源码文件中各符号地址是由编译器来规划的。
编译器的工作就是给各符号编址。编译器根据所在硬件平台特性,将源代码中的每一个符号(指令和数据)都按照硬件平台的特性分配空间,在不考虑对齐情况下,这些符号都在空间上彼此相邻,连续分布,它们在程序中距第一个符号的距离便是他们载程序中的地址。(ps:跟文件文件偏移一个意思)
本质上,程序中各种数据结构的访问,就是通过“该数据结构的起始地址+该数据结构所占内存大小决定的”来实现的。这就解释了为什么要给出变量类型,因为变量类型规定了变量所占内存的大小,每种类型都有其对应的内存容量。
程序中定义的任何一个变量,在编译后的可执行文件中都会占据一席之地。此变量在文件中的位置是编译器来安排的。编译器无论怎么安排程序中的数据,必然有一个先后顺序,而占据第一位的数据,其地址便是整个程序的起始地址,在它后面的数据依次排开。

0x1.2 什么是section

编译器提供的关键字Section只是为了让程序员在逻辑上将程序划分成几个部分,因为它是伪指令,CPU不知道这是什么东西。一般section的应用场所是根据不同的属性人为地将程序划分几部分,如数据放在一个section中,指令放在另一个section中,这样程序员编便将指令和数据分开了,使代码清晰明了,更易维护。程序如何划分并没有规定,完全看程序员喜好,甚至可以利用section把程序切的零碎不堪。

0x1.3 什么是vstart

vstart是实模式下的虚拟起始地址。作用是为section的数据指定一个虚拟的起始地址,也就是根据此地址,在文件中是找不到相关数据的,是虚拟的,假的,文件中所有符号都不在这个地址上。它只是告诉编译器将所有的区段起始地址替换成程序员所设置的地址,再无它意。编译器没有加载功能,只有编译功能。真正的程序加载是由加载器来做的。加载器会读取程序员设置的vstart地址,并把程序加载在vstart设置的内存地址上。
mbr用vstart=0x7c00来修饰的原因,是因为开发人员知道mbr要被j加载器(BIOS)加载到物理地址0x7c00,mbr后续的物理地址都是0x7c00+。另外,BIOS进入mbr是通过jmp 0:0x7c00来实现的,故此是cs已经变成0,相当于平坦模式了,只不过此时平坦模式大小只有65535字节,不是4GB。所以mbr中个数据编译出来的地址(大于等于0x7c00)实际上都成了偏移地址,这样“0*16:0x7c00+”来访问到被加载到0x7c00的mbr是正确无误的。所以,用vstart的时机是:我预先知道我的程序将来被加载到某地址处。程序只有加载到非0地址时vstart才是有用的,程序默认起始地址是0。
由于实模式下回使用段地址乘16,所以我们在实模式下设置vstart时要事先除16。注意,保护模式下段寄存器中的值是不用乘16的。

0x2 CPU的实模式

0x2.1 CPU的工作原理

CPU大体分为三部分:控制单元、运算单元、存储单元。
控制单元是CPU的控制中心,CPU需要经过它的帮助才知道自己下一步需要做什么。而控制单元大致分为指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)、操作控制器OC(Operation Controller)组成。程序被加载入内存后,也就是指令这时候都在内存里了,指令指针寄存器IP指向内存中下一条待执行指令的地址,控制单元根据IP寄存器的指向,将位于内存中的指令逐个装载到指令寄存器中,但是它还是不知道这些指令是干什么的。然后指令译码器将位于指令寄存器中的指令按照指令格式来解码,分析操作码是什么,操作数在哪里之类的。操作码就是大家平时用的mov、jmp等。寻址方式又有好多种,如基址寻址、变址寻址等,操作数类型中记录的是用哪些寄存器之类的。如果在指令中用到了立即数,就要将其记录到指令格式中立即数的部分,如果寻址用了偏移量,就要将此偏移量记录到指令格式中的偏移量部分。
存储单元是指CPU内部的L1、L2缓存及寄存器,待处理的数据就存放在这些存储单元中,这里的数据是指令中的操数。
为什么数据已经在内存中了还非得CPU内存再整这么个数据单元?
原因是缓存基本上都是采用的SRAM(Static RAM)存储器,从名字上看就知道它是一种具有静态存取功能的存储器。SRAM与DRAM不同,它不用刷新电路也能保存内部数据,这就是静态的含义。但是它集成度较低,相同容量之下,SRAM的体积比DRAM要大很多。
运算单元负责算数运算(加减乘除)和逻辑运算(比较、位移),它从控制单元哪里接受命令(信号)并执行。
CPU的总流程:控制单元要取下一条待运行的指令,该指令地址在程序技术期PC中,在x86CPU上,程序技术器就是cs:ip。于是读取ip寄存器后,将此地址送上地址总线,CPU根据此地址便得到了指令,并将其存入到指令寄存器IR中。这时指令译码器ID根据指令格式检查指令寄存器中的指令,先确定操作码是什么,在检查操作数类型,若是在内存中,就将相应的操作数从内存中取出来放入自己的存储单元,若操作数在寄存器中就直接使用。凑齐了操作码操作数后,操作控制寄存器给运算单元下令,于是运算单元便真正开始执行指令了。

0x2.2 实模式下的寄存器

寄存器是一种物理存储元件,只不过它比一般存储介质要快,所以在CPU内部有好多这样的寄存器来给CPU存储数据。
缓存成功解决了速度不匹配设备之间的数据传输,并且在一般情况下IO是整个系统的瓶颈,缓存的出现,有效减少了低速IO设备的访问频率,从而大幅度提升了速度。
段寄存器CPU是通过段基址+段内偏移地址的形式来访问内存的。段寄存器就是用来存储段基址的,它的作用就是指定一片内存的起始地址,故也称为段基地址寄存器。
访问内存,是要通过地址总线,给地址总线一个数字(也就是地址),地址总线就能找到改数字为地址的内存。可是这个数字哪来的呢?对于首次访问内存之前,其内存地址肯定是要放在与内存不同的存储介质中更合适,也更容易。如果用内存来存储内存地址,首先访问内存就是问题。那么提交给地址总线的数字从哪来的?初次访问内存时,该地址要么用立即数,要么存储在某个存储器中能让CPU取出来再访问内存,肯定不能用内存本身来存。由于寄存器比内存更高级,CPU更能接受,所以就用寄存器来存储内存地址。由于要指定的是内存中的一段区域的起始地址,所以称为段基址寄存器,也称段寄存器,无论在实模式下还是保护模式下,他们都是16位。
代码段就是把所有指令都连续排放在一起了,形成了一个全部都是指令的区域,里面存储的是指令的操作码及寻址方式等。该区域可以在磁盘上的文件中,也可以是被加载后的内存中,总之是与段指令区域。他们内部都是紧凑挨着的,内容形式完全一样,只是存放的介质不一样。代码段寄存器cs就是用来指向内存中这段指令区域的起始地址。
数据段和代码段类似,只是这段区域中的内容不是指令,而是纯粹的数据,也就是说里面存储的是程序运行所需要的数据,数与指令的操作数。数据段寄存器DS便是用来指向此数据区域的起始地址。
ip寄存器是不可见寄存器,cs段寄存器是可见寄存器。这两个寄存器相互配合就指向了CPU要执行的指令地址。
立即数就是直接存在指令中的常数,CPU在读取指令时就可以直接将这个数拿出来当作指令解析,不用再去找寄存器和内存,题现了其高效性。

0x2.3 实模式下的ret

在实模式下调用call指令时,CPU会判断要修改的PC是否跨段访问操作。如果有就将cs:ip全部保留到栈中,如果没有跨段就只保留ip。ret只将栈顶2字节数据弹出,并用它为ip寄存器赋值。只余内容正确性应由程序员自己控制。ret只置换了IP寄存器,也就是说不用换段基址,属于近返回。
retf弹出栈顶4字节数据,栈顶处的2字节用来替换IP寄存器,另外的2字节用来替换CS寄存器。同样retf也不会去检查从栈顶往上的4字节内容是不是偏移地址和段基地址,它只负责弹出它们,并将它们分别在如代码段寄存器CS和指针寄存器IP。由程序员负责栈中数据的正确性。retf称为远返回。

0x2.4 实模式下的call

在8086处理器中,有两个指令用于改变流程。一个是jmp,另一个是call。它们的区别是jmp不会像call一样保存返回有效地址。
16为模式相对近调用
call指令所调用函数和当前代码段是同一个段,即在64KB空间内,所以只给出段内偏移地址就好,不用给出段基地址。
指令中的立即数地址可以是被调用的函数名、标号、立即数,函数名同标号一样,最终会被转换成一个实际数字地址。如call near prog_name,不过千万不要误会编译后的操作数最终会被编译器转换为一个绝对地址,在编译后的机器码的操作数中,它是指令相对于目标地址的偏移量,是个地址差。也就是说,假如proc_name被编译器分配的地址是0x1234,call指令最终操作数并不是0x1234,而时目标地址减去当前call指令的地址,所得差再减去此指令长度3,最终的结果才是call相对于近调用指令操作数。
由于此操作数是相对并不是目标函数的绝对地址,只是对于目标函数地址的相对增量,所以此操作数并不能直接被CPU使用(“直接”就是操作数以立即数形式给CPU后,CPU拿来就用,不用转换)。CPU在实际执行中还要将此增量还原成绝对地址。所以此相对近调用并不能称为“直接”相对近调用。
既然是相对量,就有正负之分。如果目标地址比当前call指令大,地址相对量则为证书。如果目标地址比当前call指令地址小,地址相对量便为负数。由此可见操作数是一个有符号数。由于段是个16位大小的空间,所以,正负数的范围是-32768~32767。
为什么CPU要使用相对地址呢?
这是和硬件相关的内容,在同一段内的函数调用(近调用),必须要用相对地址的形式,这是硬件设计问题,工程师们只设计了这一种形式。偏移地址或绝对地址只是个数字,从数值上无法区分这是哪类地址,硬件一律认为给他的操作数就是相对地址(即使输入的是绝对地址)。想要让CPU工作正确,就要确保给它输入的是真正的相对地址。
以下是示例代码。

call near near_proc
jmp $
addr dd 4
near_proc:
	mov eax,0x1234
	ret

输入nasm -o call.bin call.S将汇编代码编译成二进制文件call.bin。
使用xxd命令查看call.bin文件里有什么,下面是作者提供的xxd脚本注释我们可以直接使用。

#usage: sh xxd.sh 文件 起始地址 长度 
xxd -u -a -g 1 -s $2 -l $3 $1 

#-u  use upper case hex letters. Default is lower case.
#
#-a | -autoskip
#	    toggle autoskip: A single ’*’ replaces nul-lines.  Default off.
#
#-g bytes | -groupsize bytes
#     separate the output of every <bytes> bytes (two hex characters or eight bit-digits each) by a whitespace.  Specify -g 0 to
#    suppress grouping.  <Bytes> defaults to 2 in normal mode and 1 in bits mode.  Grouping does not  apply  to  postscript  or
#    include style.
#
#-c cols | -cols cols
#            format <cols> octets per line. Default 16 (-i: 12, -ps: 30, -b: 6). Max 256.
#
#-s [+][-]seek
#   start at <seek> bytes abs. (or rel.) infile offset.  + indicates that the seek is relative to the current stdin file position 
#   (meaningless when not reading from stdin).  - indicates that the seek should be that many characters from the end of
#   the input (or if combined with +: before the current stdin file position).  
#   Without -s option, xxd starts at  the  current file position.

查看编译后的二进制文件大小ll -b call.bin,发现是13字节。
在这里插入图片描述
接下来用xxd命令查看二进制文件的内容。输入xxd -g 1 -u call.bin让每组以一个字节显示。
在这里插入图片描述
CPU遇到机器码0xe8,它就会知道这是相对近调用指令,其操作数是两个字节的数字,总共长度是3字节。EB FE是jmp $的机器码,EB是操作码,FE是操作数,由于操作数是有符号的,所以表示-2。04000000是定义的4字节数据addr dd 4。B8 3412这三个字节中B8是mov的操作码,3412是立即数。
第二种调用方式
16位实模式间接绝对近调用
“间接”是指目标函数地址没有直接给出,要么地址在寄存器中,要么在内存中,总之不以立即数的形式出现。
“绝对”是指目标函数地址是绝对地址,不像“16位相对近调用”中的那样是相对地址。
还有一点,这也是近调用,即只能调用同一个段代码中的函数,依然是只给出段内偏移就好。不用给出段基地址。
指令的一般形式是“call寄存器寻址”或“call内存寻址”,如call ax,call [0x1234]。不同的指令形式对应不同的操作码,“call 内存地址”对应的操作码是ff16,机器码是ff16+16位内存地址。机器码除了与寻址方式有关外,还和寄存器名字有关,如“call ax”的机器码是ffd0,“call cx”的机器码是ffd1,其他形式的机器码或操作吗不单独列出。
此调用形式也是近调用,调用名称中和“近”有关的就可以用near。near也可以省略。由于是近调用,并没有跨段,所以call指令只要保留IP寄存器的值就好,将其压入栈后再用新的地址偏移替换IP的值。
注意,寄存器寻址中,若在寄存器名称前添加数据类型伪指令,编译器会报警告:“warming: register size specification ignored”。警告信息字面上的意思是寄存器大小被忽略。只是提示警告,不影响编译,编译的机器码依然是正确的。
near的意思同数据类型伪指令word一样,是指在内存地址处取2字节内容,或者将操作数强制转换位2字节。可以认为像near、short、far,这些用在调用或转移中的修饰符。每种数据类型大小不同,即表示数的范围不同,用不同的范围来表示不同的调用或转移范围。
near若在寄存器前面,如call near ax,表示在寄存器读取2字节,相当于给ax寄存器中的值做了类型转换。由于near的范围可正可负,是个有符号数范围,所以它不等同于数据类型word。在这种情况下,编译器发现16位的寄存器的值精度被破坏了(寄存器中原值没变,被提取出来的数被强制类型转换了)。
同理far、short也一样,far表示4字节、short表示1字节,如果在寄存器前用这些数据类型,如call far ax或call short ax,编译器同样会发出警告。
第三种调用方式
16位实模式直接绝对远调用
何为直接?直接就是操作数在指令中直接给出,是立即数。
在各种转移指令中,凡是包含“直接”,都意指不需要经过寄存器或内存,操作数以立即数的形式给出。凡是包含“远”,就意指要跨段了,目标函数和当前指令不在同一个段中。
由于是远调用,所以cs和ip都要用新的,call指令将来还是要回来的,所以要在栈中保留回来的路,即先把老的cs寄存器压入栈,再把老的IP寄存器压入栈后,用新的CS和IP寄存器替换。
指令形式是:call far 段基址(立即数):段内偏移地址(立即数)
形式如call 0:far_proc
远调用call要用retf来配合。
CPU不会判断新的段值是否和旧的段值相等与否,而是让它加载,它就会进行加载。不会考虑新旧段基址是否相同。
注意这种远调用的方式,填写的是段内绝对地址。
第四种调用方式
16位实模式间接绝对远调用
这回看名字就知道,使用寄存器或内存存储绝对地址的调用方式。(作者的介绍总算没白费),还要注意一点,远跳的话需要4个字节,1个寄存器绝对存不下,所以至少需要两个寄存器。又因为寄存器资源十分宝贵,所以干脆只支持内存寻址。
16位间接绝对远调用的指令格式是:call far 内存寻址,如call far [bx],call far [0x1234],操作码是ff1e,前(低)2字节是段内偏移地址,后(高)2字节是段基址。在此调用方式中一定要加个关键字far,否则就和第2种间接绝对近调用一样了。

0x2.5 实模式下的jmp

无条件跳转,是指“生硬地”改变CPU的航线,将程序流转移到新的位置。CPU的航线是cs和ip,所以jmp指令也是通过修改这两个寄存器来为CPU导航。
jmp指令和call的唯一区别就是只修改cs和ip,不保存它们的值,所以跳转到新的地址后没办法再回来。
和call一样,按远近(是否跨段)来划分,大致分为两类,近转移、远转移。不过在转移方式中,还有个更近的,叫短转移。
一共有5类转移方式
16位实模式相对短转移
其实在介绍call指令的时候我们就见识过相对短转移了,就是那个常用的jmp $ 。指令格式是jmp short 立即数地址。
此处立即数地址可能是标号,因为标号只是更为人性化的立即数形式,在编辑阶段将被分配为某个地址。
和call指令一样,既然是相对的,那么这个操作数就是相对增量,有正负之分。相对短转移的机器码大小是2字节,操作码是0xeb,可知其字节大小。操作数占大小1字节。
16位实模式相对近转移
该相对转移就是比正常的短转移扩大了转移范围。
16位实模式间接绝对近转移
16位实模式直接绝对远转移
形式jmp cs:ip
16位实模式间接绝对远转移
形式jmp far [addr]

0x2.6 标志寄存器flags

有条件转移指令jcc的条件转移是依赖标志寄存器中的标志位的,实模式下的标志寄存器是flags。
flags寄存器中存储的信息只是结果的特征,即标志,并不是真正的结果,结果可以存储在内存中。
flags寄存器是16位宽,保护模式下对其拓展(extend)成32位的eflags寄存器。
以下标志位仅在80286以上CPU有效,相对8088,它支持特权级和多任务。
第12~13位,IOPL位,即Input Output Privilege Level,这用在有特权级概念的CPU中。有4个任务特权级,即特权级0/1/2/3。故IOPL要占用2位来表示这4个特权级。
第14位为NT,即Nest Task,意为任务嵌套标志位。8088支持多任务,一个任务就是一个进程。当一个任务中又嵌套了另一个任务(进程)时,此NT位为1,否则为0。
以下标志位80386以上的CPU才支持。
第16位RF位,即Resume Flag,恢复标志位。该标志位用于程序调试,指示是否接收调试故障,它需要与调试器一起使用。当RF位1时忽略调试故障,为0时接受。
第17位VM位,即Virtual 8086 Model,意为虚拟8086模式。这是实模式向保护模式的产物,现在已经没有了。
第18为为AC位,即Alignment Check,意为对齐检查。检擦程序中的数据活指令其内存地址是否是偶数,是否是是16、32的整数倍,没有余数,这样硬件每次对地址以自增地方式(每次加2、16、32等)访问内存时,自增后的地址正好对齐数据所在的起始地址上,这就是对齐原理。对齐不是软件逻辑要求,而是硬件要求,如果访问是16或32的整数倍,硬件上好处理,所以运行较快。若AC位为1时,则进行地址对齐检查,为0时不检查。
以下标志位值对80586(奔腾)以上CPU有效
第19位为VIF位,即Virtual Interrupt Flag,意为虚拟中断标志位,虚拟模式下的中断标志。
第20位为VIP位,即Virtual Interrupt Pending,意为虚拟中断挂起标志位。在多任务下,位操作系统提供的虚拟中断挂起信息,与VIF位配合。
第21位为ID位,即Identification,意思为识别标志位。系统经常要判断CPU型号,若ID为1,表示当前CPU支持CPU id指令,这样便能获取CPU的型号、厂商等信息。若ID为0,则表示当前CPU不支持CPU id指令。
其余剩下的22~31 为都没有实际用途,纯粹是占用位,为了将来拓展。
实模式最后被保护模式淘汰的原因,最主要是安全隐患。
在实模式下,用户程序和操作系统可以说是同一特权的程序,因为实模式下没有特权级,他处处和操作系统平起平坐,所以可以执行一些破坏性指令。(各种病毒程序就是典型的例子)。

0x3 让我们直接对显示器说点什么

0x3.1 CPU如何与外设通信——IO接口

现实生活中的各种硬件由各种厂商负责提供,它们的种类繁多,原理个不行相同。他们都有字节的特性,数据格式不同,有的外设用串行数据,有的是用并行数据,并且让他们都在自己的时序下工作,无论他们的速度如何,在CPU看来都太慢了。
而且CPU不可能记住每个硬件的访问方式,而且CPU熟读那么快,外设的速度没法与CPU相匹配,为了减少自己的等待时间,还得为这些低速设备准备数据缓冲区。CPU用的信号都是TTL电平,外设大多数都是机电设备,机电设备可不能用TTL电平驱动,CPU系统总线上传送的都是并行数据(所以你听到的都是8位、16位、32位CPU),外设可是并行、串行都有,还得转换格式,不可能让CPU去一一适应它们,否则CPU做的工作就太多了。
于是CPU设计师们给CPU和外设之间添加了一个代理,CPU以后想要访问各种设备只需要找到对应的代理,让这个代理再进行数据格式的转换发给外设处理即可。什么速度不匹配、缓冲区的问题都由代理解决。举个例子,如果CPU想要跟串行设备通信,CPU就同串行接口进行通信,把数据发给它后,数据再经由串行接口发给串行设备,串行设备有了反馈后,把数据发送给串行接口,串行接口再返回给CPU,并行设备也是如此。
任何不兼容问题都可以通过增加一层IO接口代理的方式解决,IO接口形式不限,它可以是一个电路板,也可以是块芯片,甚至可以是一个插槽,它的作用就是在CPU和外设间做协调转换,如CPU和外设之间速度不匹配,它就起到了变速箱的作用,CPU和外设信号不通,它就是翻译机。
再具体点就是,音箱中的声卡就是驱动音响设备的IO接口。显卡也同样是一种IO接口,它是用来驱动显示器的。现在电脑的声卡和显卡已经被集成在主板芯片组中了,我们常见的集成声卡和集成显卡就是。
IO接口时接连CPU与外部设备的逻辑控制部件,可以分为硬件和软件两部分,硬件部分功能是协调CPU和外设之间的种种不匹配,如双方由于速度不匹配,那IO接口就实现数据缓冲以减少等待时间,数据格式不匹配,IO接口就进行格式转换。IO接口实际上内部也是由软件控制工作的,这就是所谓的逻辑部分,所以软件是指用来控制接口电路工作的驱动程序以及完成内部数据传输所需要的程序。
IO接口芯片又分为可编程接口芯片和不可编程接口芯片。
接口的作用是连接处理器和外部设备,如果外部设备工作很简单,不需要设定就直接可以执行功能就使用不可编程芯片,不可编程芯片是很简单的IO接口。
当我们需要一个IO接口实现多种模式和多种复杂功能时,且允许多种不同外设都可以连接该接口进行通信,这时就需要用计算机指令告诉IO接口,那些设备连接在IO接口上,此IO接口的工作模式等。这种通过软件选择IO接口上的功能、工作模式的做法,称为“IO接口控制编程”。这通常时用端口读写指令in/out实现的。
为了简化CPU对外设的访问工作,大家统一约定好IO接口的功能:
1)设置数据缓冲,解决CPU与外设间的速度不匹配
CPU和外设速度上的差异可以通过设置缓冲区来解决,数据先存储在缓冲区里,等待需要时就传送出去。
2)设置信号电平转换电路
CPU和外设的信号电平不同,CPU所用信号电平事TTL电平,而外设大多数是机电设备,不能用TTL电平驱动,可以在接口电路中设置电平转换电路来解决。
3)设置数据格式转换
外设的多种多样决定了输出的信息可能是数字信号、模拟信号等,而CPU只能处理数字信号。数字信号需要经过数/模转换(D/A)成模拟量才能被送到外设以及驱动硬件。模拟量也同样需要经过模/数(A/D)转换成数字信号才能被处理。
4)设置时序控制来同步CPU和外部设备
接口电路要协调CPU和硬件的两种不同的时间计发。
5)提供地址译码
CPU同多个硬件硬件打交道,每个硬件反馈信息信息也很多,所以一个IO接口必须包含多个端口,即IO接口上的寄存器,来存储这些信息内容。但同一时刻,只能有一个端口和CPU数据交互,这就需要IO接口提供地址译码电路,使CPU可以选中某个端口,使其可以访问数据总线。
CPU通过总线访问各个物理设备。CPU只能同一时间访问到一个物理设备对应IO接口的某一个端口。当多个物理设备都想要和CPU进行通信时,还需要添加一层专门用来筛选IO接口的电路来仲裁IO接口,它的名字就叫做输入输出控制中心(I/O control hub,ICH),也就是南桥芯片。对应名字的北桥芯片的上部分就是散热片,下面的才是真正的北桥。由于南桥和北桥是成对出现的,南桥主要用于链接低速设备,北桥用于链接高速设备。
CPU通过内部总线连接到南桥芯片的内部,这个内部总线是专用的,他只通向位于南桥中的CPU接口。在南桥内部集成了一些IO接口,如并口硬盘PATA(就是我们平时说的IDE硬盘)、串口硬盘SATA、USB、PCI设备、电源管理等设备接口是直接内置在南桥的内部。
为了支持非必要设备的拓展,南桥提供了专用拓展的接口,这就是PCI接口。在主板上有很多插槽,他们就是预留的pci接口,pci设备可以即插即用。由于他们延伸到了南桥外面,且可以很多pci设备连接上来,这条延长的pci接口变成了PCI总线。总线就是外设都要连接的,用于传输数据、行为控制的电线。
IO接口设计之初,就要被设计成要通过寄存器方式通CPU通信,其内部有专用数据交互的寄存器,只不过这里所说的寄存器位于IO接口中,为了区别与CPU内存的寄存器,IO接口中的寄存器就称为端口(这可不是网络应用程序所开的那种端口)。
IO接口是CPU与硬件的桥梁,一端是CPU,另一端是硬件。端口是IO接口开放给CPU的端口,一般的IO接口都有一组端口,每个端口都有自己的用途,甚至有时,一个端口在不同情况下有不同用途。可见IO端口另一端的硬件还是很复杂的。
端口也是寄存器,寄存器有数据宽度,有8位、16位、32位,各个设备是不同的这取决于生产厂商。
访问端口的方法:
既然外设中的ROM可以通过内存映射来访问,端口也可以,把一些内存作为端口的映射,访问这些内存就相当于访问这些端口。还可以将端口独立编址,把所有端口从0开始编号,位于IO端口上的所有端口号都是连续的。
IA32体系系统中,因为用于存储端口号的寄存器是16位的,所以最大有65536个端口,即0~65535。
要是通过内存映射,端口就可以用mov指令来操作。但由于用的是独立编址,所以就不能把它当作内存来操作,因此CPU提供了专门的指令来干这事,in和out。
in指令用于从端口中读取数据,其一般形式是:
1)in al,dx
2)in ax,dx
其中al和ax用来存储从端口获取的数据,dx是指端口号。
这是固定用法,只要用in指令,源操作数(端口号)必须是dx,而目的操作数是用al,还是ax,取决于dx端口代指的寄存器是8位宽度,还是16位宽度。
out指令用于端口中写数据,其一般形式是:
1)out dx,al
2)out dx,ax
3)out 立即数,al
4)out 立即数,ax
注意,这里与in指令相反,in指令的源操作数是端口号,而out指令中的目的操作数是端口号。
in和out指令的共性
1)在以上两个指令中,dx只做端口号之用,无论其是操作数或目的操作数。
2)in指令从端口读数据,可以认为端口是数据源,所以端口出现在源操作数的位置。读出来的数据要有个“目的地”来存放,所以in指令中存放数据的地方出现在“目的操作数”位置。
out指令是把数据写入端口指向的寄存器,在这里,端口是数据的“目的地”,所以端口出现在目的操作数的位置。待写入的数据总有个“来源”,所以out指令中的“源操作数”是数据来源。
3)在以上两个指令的两个操作数中,无论是对于源操作数,还是目的操作数,除端口外,那个作为数据的操作数,一律用al寄存器存储8位宽度的数据,用ax寄存器存储16位宽度的数据。
4)in指令中,端口号只能用dx寄存器。
5)out指令中,可以选用dx寄存器或立即数充当端口号。

0x3.2 显卡概述

上一章直接在文本模式下,对显卡映射内存书写字符串让其在显示器上显示的方式现在早就不用了。
mbr运行在实模式下,所以在实模式下也可以用BIOS的0x10中断打印字符串,因为中断向量表只存在于实模式下,BIOS中断是要依赖中断向量的。不适用于接下来要进入的保护模式,也就没有BIOS中断向量表。其次,不希望有更多依赖BIOS软件。
某些IO接口也叫适配器,适配器是驱动某一外部设备的功能模块。显卡也称为显示适配器,不过归根结底它就是IO接口,专门用来连接CPU和显示器。我们想要操作显示器,没有直接的办法,只能通过它IO接口——显卡。
显卡是pci设备,所以是安装在主板上的pci槽上的,pci总线式共享并行架构,并行数据就要保证数据发送好必须同时到达目的地,因为这关系到数据的顺序。例如8位并行总线就需要同时发送这8位,接收方也要同时接收这8位才行。虽然并行效率很高,但是对于要保证同时接收n位数据,这是有困难的,随着并行数据位宽越来越大,这种困难也越来越明显。于是串行传输很好地解决了这一问题,一次只发一位,这样顺序就解决了,数据到目的地看再组合到一起就成了。于是就有了PCI Express总线,这就是串行设备简称pcie。现在的显卡都是串口的了。不要绝得传输速度一定是并行快,因为传输速度一部分取决于并行数据量,一部分还要取决于传输频率。串口显卡虽然一次只能传输1位,但它的传输频率高。

0x3.3 显卡、显存、显示器

为了能看到图像,我们需要显示器。无论哪种显示器,它都是由显卡控制的,我们没必要了解液晶显示和普通CRT显示器的差别。无论哪种显卡,它提供给我们的编程接口都是一样的:IO端口和显存。
显存是由显卡提供的,他是位于显卡内部的一块内存,所以称它为显存。标注了DDR 512M,DDR2 1G这些都是指显存大小。显卡的工作就是不断的读取这块内存,随后将其内容发送到显示器。
显示器上多彩的图案,说明显卡可以让显示器工作再图形模式,能在显示器上看到Linux终端上的黑屏白字,说明显卡可以让显示器工作在字符模式。屏幕是有密密麻麻的像素组成的,显存中的每一位都对应了一个像素点。
在黑白图像模式中,显存位与像素是1对1,因为只有两种颜色,所以只要现存中对应位置是1,屏幕上响应像素就被点亮,呈现的是白色。若该位为0,该像素就不会被点亮,只要不管该像素就是黑色,所以用黑色壁纸当桌面,才是在物理上保护显示器。
文本模式的屏幕上其实是由两个字节来表示一个字符的,低字节是ASCII码,高字节是对应的字节属性,这就是为什么文本模式还能显示颜色的原因。
高字节中,低4位是字符前景色,高4位是字符的背景色。

0x3.4 改进MBR,直接操作显卡

;主引导程序
;
;LOADER_BASE_ADDR equ 0xA000
;LOADER_START_SECTION equ 0x2
;---------------------------------------------------------
SECTION MBR vstart=0x7c00
	mov ax,cs
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00
	mov ax,0xb800
	mov gs,ax
;清屏
;利用0x06功能号,上卷全部行,则可清屏
;---------------------------------------------------------
;INT 0x10	功能号:	0x06	功能描述:	上卷窗口
;---------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值
	mov ax,0600h
	mov bx,0700h
	mov cx,0
	mov dx,184fh
	int 10h
	;输出背景色绿色,前景色红色
	mov byte [gs:0x00],'1'
	mov byte [gs:0x01],0xA4;A表示绿色背景闪烁,4表示前景色为红色
	
	mov byte [gs:0x02],' '
	mov byte [gs:0x03],0xA4

	mov byte [gs:0x04],'M'
	mov byte [gs:0x05],0xA4
	
	mov byte [gs:0x06],'B'
	mov byte [gs:0x07],0xA4

	mov byte [gs:0x08],'R'
	mov byte [gs:0x09],0xA4

	jmp $	;通过死循环悬停再此

	times 510-($-$$) db 0
	db 0x55,0xaa

编译后重新将mbr.bin写入虚拟磁盘
在这里插入图片描述运行bochs,查看是否有绿色背景的字符。发现右上角有绿色背景,红色字符串在不停闪烁。
在这里插入图片描述
在这里插入图片描述

0x3.5 MBR操作硬盘

0x3.5.1 硬盘控制器端口

首先给出作者提供的硬盘控制器端口表,这个表种名为Device的端口就是专门指定要操作硬盘的端口。
在这里插入图片描述
端口可以分为两组,Command Block registers 和 Control Block registers。Command Block registers用于向硬盘驱动器写入命令或者从硬盘控制器获取硬盘状态,Control Block registers用于控制硬盘状态。
端口是按照通道给出的,一个通道上分别由主、从两块硬盘,都用这些端口号。想要操作某通道上的某块硬盘,需要单独指定对应8位的与磁盘相对应的寄存器,不过由于只是指定某个磁盘,只需要1位就够了(寄存器是很宝贵的),所以除了指定磁盘,其中第4位,便是指定通道上的主或从硬盘,0为主盘,1为从盘(一个通道上有两个分支,主盘通道分支和从盘通道分支)
在这里插入图片描述
端口用途在都硬盘和写硬盘时还是有区别的,比如拿Primary通道上的0x1F1端口来说,读操作时若读失败,里面存储的时失败状态信息,所以称为error寄存器,并且0x1F2端口中存储未读的扇区数。在写操作时error寄存器又变成了feature寄存器,此寄存器用于写命令的参数。
Data寄存器在名字上我们就知道它是用于管理数据的,我们读取和写入的数据都通过这个寄存器获取。为了让读写数据更快,此寄存器被设计为16位。在读硬盘时,硬盘准备好数据后,硬盘控制器比将其放在内部的缓冲区,不断读此寄存器便是读取缓冲区的全部数据。在写硬盘时,我们把数据源源不断的送到此端口,数据便被存入缓冲里,硬盘发现缓冲区中有数据了,编讲此数据写入相应的扇区。
读磁盘时,端口0x171或0x1F1的寄存器叫Error寄存器,只在读取硬盘失败时才有效,里面才有失败的信息,尚未读取的扇区数载Sector count寄存器中。在写磁盘时,有些命令需要添加额外的参数,此寄存器就是用来填写写入命令参数的。
Sector count 寄存器是用来指定待读取或待写入的扇区数。硬盘每完成一个扇区,就会将此寄存器的值减1,所以如果中间读或写操作失败了,该寄存器中的值便是未完成的扇区。这是8位寄存器,最大值为255,若指定为0则表示要操作256个扇区。
硬盘中的扇区在物理上是用“柱面-磁头-扇区”来定位的(Cylinder Head Sector),简称CHS,但是每次我们都要事先计算好扇区的具体位置过于麻烦。于是我们希望有一套对于人来说比较直观的寻址方法,我们希望磁盘中扇区从0开始依次递增编号,不用考虑扇区所在物理结构。这是一种逻辑上为扇区编址的方法,全程逻辑块地址(Logical Block Address)。
LBA有两种,一种是LBA28,用28个位bit来描述一个扇区的地址。最大寻址范围是2的28次方等于268435456个扇区,每个扇区是512个字节,最大支持128G。另一种是LBA48,用48bit来描述一个扇区的地址,最大可寻址范围是2的48次方,等于281474976710656个扇区,乘以512字节后,最大支持131072TB,即128PB。
LBA寄存器有LBA low、LBA mid、LBA high 三个,它们三个都是8位宽度的。LBA low寄存器用来u才能出28位地址的0 ~ 7位,LBA mid寄存器用来存储第 8 ~ 15 位,LBA high寄存器存储第16 ~ 23位。剩下不够的位数引出Device寄存器。
device寄存器是个杂项,它的宽度是8位。在此寄存器的低4位用来存储LBA地址的24~27位。结合上面的LBA寄存器。第4位用来指定通道上的主盘或从盘,0代表主盘,1代表从盘。第6位用来设置是否启用LBA方式,1代表启动LBA模式,0代表启用CHS模式。另外两位固定为1,称为MBS位,不需要关注。
在读硬盘是,端口0x1F7或0x177的寄存器名称是Status,它是8位寄存器,用来给出硬盘的状态信息。第0位是ERR位,如果此位为1,表示命令出错了,具体原因可见error寄存器。第3位是data request位,如果此位为1,表示硬盘已经把数据准备好了,主机可以把数据读出来。第6位是DRDY,表示硬盘就绪,此位是在对硬盘诊断时使用的,表示硬盘检测正常,可以继续执行一些命令。第7位BSY,表示硬盘是否繁忙,吐过位1表示硬盘繁忙,此寄存器其他位都无效。另外4位暂不关注。
在写硬盘时,端口0x1F7或0x177寄存器的名称是command,此寄存器用来存储让硬盘执行的命令,只要把命令写进此寄存器,硬盘就开始工作了。在书上的系统中,主要使用了3个命令。
1)identify:0xEC,硬盘识别
2)read sector: 0x20,读扇区
3)write sector: 0x30,写扇区
常用寄存器端口示意图,左边是device,右边是status寄存器。
在这里插入图片描述

0x3.5.2 常用硬盘操作方法

最权威的使用方法是去参考ATA手册。
不管是读硬盘,还是写硬盘,都不是一个指令就能完成的。相关寄存器都要设置。要是读硬盘,首先要告诉读哪个扇区,读几个扇区,用哪种模式寻址。写硬盘也是一样。
一般操作硬盘最主要顺序就是command寄存器一定得是最后写,因为一旦command寄存器被写入后,硬盘就开始干活了。其他寄存器顺序不是很重要。
1)先选择通道,往该通道的sector count写入待操作扇区数
2)往该通道上三个LBA寄存器写入扇区起始地址的低24位
3)往device寄存器中写入LBA地址的24~27位,并置第6位为1,使其为LBA模式,设置第4位选择主盘或从盘
4)往该通道上command寄存器中写入操作命令
5)读取该通道上的status寄存器,判断硬盘工作是否完成
6)如果以上步骤是读硬盘进入下一个步骤,否则完工。
7)将硬盘数据读出
硬盘完工后,它已经准备好了数据,怎么获取呢?一般常用的数据传送方式如下:
1)无条件传送方式
2)查询传送方式
3)中断传送方式
4)直接存储器存储方式(DMA)
5)I/O处理机传送方式
第1种,应用此方式的数据源设备一定是随时准备好了数据的,CPU随时取随时拿都没问题,CPU读取数据时不用打招呼。
第2种,也称为程序I/O、PIO(Programming Input/Output Model),是指传输前,由程序先去检测设备的状态。数据源设备在一定的条件下才可以传输数据,这类设备通常是低速设备,比CPU慢很多。CPU需要数据时,先检查该设备状态,如果状态为“准备好了可以发送”,CPU再去获取数据。硬盘有status寄存器,里面保存了工作状态,所以对于硬盘可以用此方式来获取数据。
第3种“中断传送方式”,也称为中断驱动I/O。上面提到的“查询传送方式”有这样的缺陷,由于CPU需要不断查询设备状态,所以意味着只有最后一刻的查询才有意义的,之前的查询都是发生在数据尚未准备好的时间段里,所以所效率不高,仅对于不要求速度的系统可以采用。可以改进的地方是如果数据源设备将数据准备好后再通知CPU来取,这样效率就变高了。通知CPU可以采用中断的方式,当数据源设备准备好数据后,它通过发中断来通知CPU来拿数据,这样就避免了查询花费大量的时间,效率高。
第4种“直接存储器存取方式(DMA)”。在中断传送方式种,虽然极大地提高了CPU的利用率,但通过中断方式通知CPU,CPU就要通过压栈保护现场,还要执行传输指令,最后还要恢复现场。这种方式不让CPU参与传输,完全由数据设备和内存直接传输。CPU直接到内存种拿数据,DMA是硬件,不是软件概念,所以需要DMA控制器才行。
第5种“I/O处理机传送方式”。为了完全解放CPU,在DMA控制器种把数据交换、组合、校验的功能都融合了起来,然后再引入除了DMA以外的硬件。于是I/O机理就诞生了,听名字就知道它是专门处理IO的,并且他其实是一种处理器,只不过用的时另一套擅长IO的指令系统,随时可以处理数据。有了IO机理的帮忙,CPU甚至不知道由数据传输这回事,同样,这也需要单独的硬件来支持。

0x3.6 让MBR使用硬盘

在前几章内容里,我们都是在操作MBR的内容。但是MBR只有512字节,这块空间不足以支撑操作系统的所有寄存功能,所以我们需要继续传递cs:ip。

0x3.6.1 改造MBR

我们的MBR受限于512字节大小,没办法为内核准备好环境,更没办法将内核成功加载到内存并运行。所以我们要在另一个程序中完成初始环境以及加载内核的任务,这个程序我们称为loader,即加载器。这块MBR负责从磁盘上把loader加载到内存,并将接力棒交给它。
由于MBR是占据了磁盘的0扇区(以逻辑LBA方式,扇区从0开始编号,若以物理CHS方式,扇区从1开始编号),第1扇区是空闲的,可以用,但是相隔太近,需要一段空间。将loader放到第2扇区。MBR从第二扇区把它读出来。读出来后原则是只要有空闲的地方都可以放置,之前的实模式下内存布局中,0x500 ~ 0x7BFF和0x7E00 ~ 0x9FBFF这两段内存区域都可以。
首先,loader中要定义一些数据结构(如GDT全局描述符表),这些结构式为将来内核使用的,所以loader加载到内存后不能被覆盖。
其次,随着不断添加功能,内核会越来越强大,其所在的内存地址也会向越来越高的地方发展,难免会超过可用区域的上限,所以尽量把loader放在低处,多留出空间给内核。

;主引导程序
;----------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
	mov ax,cs
	mov ds,ax
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00
	mov ax,0xb800
	mov gs,ax

;清屏
;利用 0x06功能号,上卷全部行,则可清屏
;----------------------------------------------------------
;INT 0x10	功能号:0x06		功能描述:上卷窗口
;----------------------------------------------------------
;输入
;AH 功能号= 0x06
;AL = 上卷行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角(x,y)位置
;(DL,DH) = 窗口右下角(x,y)位置
;无返回值
	mov ax,0600h
	mov bx,0700h
	mov cx,0
	mov dx,184fh
	int 10h
	;输出背景色绿色,前景色红色
	mov byte [gs:0x00],'1'
	mov byte [gs:0x01],0xA4;A表示绿色背景闪烁,4表示前景色为红色
	
	mov byte [gs:0x02],' '
	mov byte [gs:0x03],0xA4

	mov byte [gs:0x04],'M'
	mov byte [gs:0x05],0xA4
	
	mov byte [gs:0x06],'B'
	mov byte [gs:0x07],0xA4

	mov byte [gs:0x08],'R'
	mov byte [gs:0x09],0xA4

	mov eax,LOADER_START_SECTOR		;起始扇区lba地址
	mov bx,LOADER_BASE_ADDR			;写入的地址
	mov cx,1						;待读入的扇区数
	call rd_disk_m_16				;以下读取程序的起始部分(一个扇区)
	
	jmp LOADER_BASE_ADDR
;----------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;----------------------------------------------------------
									; eax=LBA 扇区号
									; bx=将数据写入的内存地址
									; cx=读入的扇区数
	mov esi,eax						; 备份eax
	mov di,cx						; 备份cx
;读写硬盘:
;1:设置要读取的扇区数
	mov dx,0x1f2
	mov al,cl
	out dx,al						;读取扇区数
	
	mov eax,esi						;恢复ax

;2:将LBA 地址存入0x1f3 ~ 0x1f6
	
	;LBA地址7~0 位写入端口0x1f3
	mov dx,0x1f3
	out dx,al
	;LBA地址15~8位写入0x1f4
	mov cl,8
	shr eax,cl
	mov dx,0x1f4
	out dx,al
	;LBA地址23~16位写入端口0x1f5
	shr eax,cl
	mov dx,0x1f5
	out dx,al
	
	shr eax,cl
	and al,0x0f						;lba 第24~27
	or al,0xe0						;设置7~4位为1110,表示lba模式
	mov dx,0x1f6
	out dx,al
;3:0x1f7端口写入读命令 0x20
	mov dx,0x1f7
	mov al,0x20
	out dx,al
;4:检测硬盘状态
.not_ready:
	;同一端口,写时表示写入命令字,读时表示读入硬盘状态
	nop
	in al,dx
	and al,0x88						;3位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘繁忙
	cmp al,0x8
	jnz	.not_ready					;若未准备好继续等待
;5:0x1f0端口读取数据
	mov ax,di
	mov dx,256
	mul dx
	mov cx,ax
; di为要读取的扇区数,一个扇区有512字节,每次读入一个字节共需di*512/2次,所以di*256
	mov dx,0x1f0
.go_on_read:
	in ax,dx
	mov [bx],ax
	add bx,2
	loop .go_on_read
	ret
times 510-($-$$) db 0
db 0x55,0xaa

要包含的文件boot.inc

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

将程序编译,这里-I代表添加依赖的目录,boot.inc就放在这个目录下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来编写loader加载器的代码

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR

; 输出背景颜色绿色,前景色红色,并且跳动的字符串“1 MBR”
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4

mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4

mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4

mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4

mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4

mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4

mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4

mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4

jmp $

输入指令进行编译nasm -I ./include/ -o loader.bin loader.S
将生成的loader.bin写入虚拟磁盘的第二扇区。第0扇区是MBR。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值