学习笔记

《真象》第四章 保护模式入门

Posted on

回过头来看以前写的代码居然有点看不懂了,还是应该写一下笔记啊……顺便吧第四章复习一下。
《真象》是指《操作系统真象还原》这是第四章笔记,保护模式入门。

  1. 保护模式起于80286,为16位的CPU,兴起于80386,32位的CPU。保护模式下一般都是32位及以上。
  2. CPU有三种模式:实模式,虚拟8086模式,保护模式。严格意义上32位CPU无实模式。
  3. 制定运行模式编译:使用法 [bits] 16/32
  4. 运行模式反转:指运行模式在16位实模式,操作数为32位;或者当前运行模式是32为保护模式,操作数为16位。操作系统兼容两种运行模式,所以支持两种操作,一般临时的反转会在指令前自动加上'66'或者'67'。(不过这实际上是编译器的工作,我们只需要知道就好了
  5. 在保护模式下,必须用ecx表示循环次数,其他都兼容。
  6. 对于段寄存器的入栈,无论指定在哪种模式下运行,都是按照当前模式的默认操作数大小压入的。而对于通用寄存器和内存,入栈是sp移动大小有操作数决定。
  7. 段描述符,8字节大小。保存着段基址和段界限和各种保护信息,用于描述一个段(代码段,栈段等),具体格式见 P151。
  8. 全局描述符表GDT,局部描述符表LDT。通俗的说,描述符表就是一个段描述符数组,所谓选择子就是数组下标。GDT是全局数组,LDT为局部数组。通过选择子在描述符表中找出段描述符,段描述符中又记录中段基址和段界限,即可得到一个段。
  9. GDT Register。全局表述符表保存在内存中,而GDT寄存器保存着这个表的位置与大小。寄存器大小为 6 字节。前2个字节为GDT界限,后4个字节为GDT内存其实地址。赋值格式 lgdt 48位数据。而 LDT Register的格式为 lldt 16位寄存器/16位内存
  10. 选择子结构。前2位用于表示选择子请求者的当前特权级别RPL,2位4个等级。第3位为类型标识,0为GDT,1为LDT。一个选择子初始化为全0。初始选择子会访问GDT的第一个段描述符,为了保护作用,GDT的第一个段空间不可用,当访问第一个段空间的时候会报错。
  11. A20Gate。在实模式下的wrap-around,当地址超过20位的时候就会发生回绕。保护模式下为了兼容实模式,有了A20Gate。A20Gate被禁止并访问20位以上时,会发生地址回绕,否则不会,也就在寻址上进入了保护模式。
  12. 保护模式的开关,CR0寄存器的PE位。将其置为1后,将在真正意义上进入保护模式。一般或1即可。
  13. 由实模式进入保护模式的三个步骤:
    1. 打开A20Gate
    2. gdtr寄存器中加载GDT的地址会界限
    3. 将CR0寄存器的pe位置1
  14. 流水线。将一个目标指令具体化为多个指令,在已知(或预测)指令的情况下,CPU并行处理多个指令,因此某段时间后的每一个时间周期都会执行完一个目标指令。当遇到jmp指令的时候,流水线会清空。
  15. 对流水线的优化有无先后逻辑的指令有乱序执行和缓存等。
  16. 在实模式进入保护模式之后,应优先清空流水线。因为实模式下的指令存储与保护模式不同,实模式会先把段基址左移后在加入缓存,这将与保护模式的指令格式不符,导致错误。
  17. 分支预测。最简单的分支预测算法是 2位预测法,用2位bit的计数器来记录跳转状态,没跳转一次加1,达到3则不再增加,否则减1,直到0则不再减了。优先计数大的,否则随机。
  18. 加载选择子时的保护:
    1. 验证段描述符是否越界。
    2. 检查段的类型。不同段的不同type允许被加载的寄存器不同。详细见P174。
    3. 检查段是否存在。CPU通过段描述符的P位来确认内存段是否存在。
  19. 代码段与数据段的保护。CPU每访问一个地址,都要确认该地址不能超过该内存段范围。确认方式为 (描述符中段界限+1) × (段界限的粒度大小:4k或者1) - 1,化简可得
    • 指令: EIP的偏移地址+指令长度-1 <= 实际界限大小
    • 数据: 偏移地址+数据长度-1 <= 实际界限大小
  20. 栈段并不一定都是向下扩展的。(这里有地方不是很明白,P175
学习笔记

《汇编语言》的一些小结

Posted on

今天汇编终于到了,马买皮,早知道这么慢就去京东上买了。

我为什么要学汇编

关于汇编,以前看《深入理解计算机系统》的时候就大致看了一蛤,当然是远远不够的,而且这本书我从大二看到现在也还没看完……
而我稍微浏览了一下我们这学期的教科书,跟那个煞笔老师一样让我感到很遗憾的是,书上也完全没有汇编。
要是真的学得这么浅的话,就不止是让人遗憾,更是让人觉得失望了。
是的,我已经很自然的认为学习操作系统就需要学并用到汇编,比如MBR还想用什么写,那是直接接触硬件的部分,除了汇编没有其他选择。emmmmm,据说汇编还有其他在debug等追根溯源上作为终极武器有很强的功能,据说啦,据说。不过看了一点之后发现汇编和操作系统在一些部分是很通用的。
另外,在《操作系统真象还原》其实最基础的假设就是读者都已经学了汇编。
顺便吐槽一句,王爽的《汇编语言》居然平台居然是Windows的,醉了。

正文

2017-10-28 00:37:44 星期六

  1. 对于一串01字符串,既可以表示命令,也可以表示数据,CPU区分的方式是通过寄存器分割开来。比如说固定让这个寄存器访问命令,让那个寄存器访问数据。
  2. 8086通过“段+偏移”的寻址方式,而使段只偏移4位的结果有二,一是CPU可以通过不同的“段+偏移”来形成同一个物理地址。二是若仅改变偏移地址,最多寻址64KB个内存单元。
  3. 8086有4个段寄存器 CS,DS,SS,ES。
  4. 8086 CPU的第一条指令固定是 FFFFH:0000H
  5. cs:ip 只能通过 jmp命令更改,单独更改 cs 段寄存器则不需要。
  6. CPU与内存交互主要通过 DS 段存储器,ds存储内存单元的段地址,再通过 [偏移地址] 访问具体内存单元。
  7. 8086 不支持直接将数据送入段存储器的操作。
  8. 1字 = 2字节。字节类型数据在8086中需要用到两个内存单元存储,高地址单元存放高8位,低地址单元存放低8位
  9. 任意时刻,SS : SP只想栈顶元素。8086 CPU只知道栈顶元素在哪,而无法知道我们安排的栈有多大。
  10. 8086 CPU在入栈时,栈顶从高地址向低地址方向增长。入栈顺序是先移动栈顶指针,再存储数据。出栈则反之。
  11. 段。段简单来说就是一大部分连续的内存单元。但是这个内存单元的起始位置必须是16的倍数。原因在于8086的寻址方式。根据在段中存放的内容,可分为“数据段”,“代码段”,“栈段”。

顺便附上我写的第一个汇编程序

assume cs:codesg

codesg segment

    mv ax,2
    add ax,ax
    add ax,ax

    mov ax,4c00h
    int 21h

codesg ends

end

虽然连结果都打印不出来,但还是挺兴奋的。

2017-10-30 11:40:50 星期一

终于可以做书上的实验了!!!
总结一下 DOS 中debug的命令。

  • r : 查看,修改CPU寄存器的内容
  • d : 查看内存的内容
  • e : 修改内存的内容
  • u : 将内存中的内容解释为机器指令和对应的汇编指令
  • t : 执行当前指令,当前指令有cs:ip 所指向,可通过改变这两个寄存器改变指令
  • a : 以汇编形式写入内存
  • p : 执行中断指令

2017-10-30 22:45:57 星期一

  1. 修改了SS寄存器后,会紧接着执行下一条指令
  2. shell——操作系统的外壳。对于一个可执行程序,都是由shell执行,将该程序加载到内存,设置CS:IP 指向程序的入口,此后程序运行,运行结束后,返回到shell。最后shell再去执行其他程序
  3. dos运行程序时,都需要利用PSP(Program Segment Prefix)来和被加载程序进行通信。PSP占据256(100H)个字节。所以程序的物理地址为 SA+10H:0
  4. SA(Segment Adress),段地址。EA(Effective Adress),偏移(有效)地址。
  5. 通常使用loop指令来实现循环功能,cx中存放循环次数。
  6. 在汇编程序中,数据不能以字母开头。比如A000H,就必须写成0A000H。
  7. 在汇编源程序中,如果用指令访问一个内存单元,则在指令中必须用“[...]”来表示内存单元。但如果“[]”内是数值常量,就必须在前面加上段前缀。所谓段前缀就是 “段寄存器:” 的形式。
  8. dos 方式下,一般情况下,0:200 ~ 0:2ff 这段空间是安全的。
  9. end 除了通知编译程序以外,还可以通知编译器程序的入口在哪。
  10. 取得程序内存的两种方法:在程序加载时系统自动为其分配,或者在程序运行时动态申请。
  11. 这里再次强调。伪指令不会被编译器所执行,只能作为程序员理清代码使用。比如最开始的assume,不能确保段的功能与assume相对应。
  12. 用'...'的形式指明数据是由字符形式给出的。编译器会将其转化成对应的ASCII码。
  13. 内存单元中的偏移地址表达形式有:(其中,idata表示数值常量)
    1. [ax+bx+idata]
    2. idata[ax][bx]
    3. [ax].idata[bx]
    4. [ax][bx].idata

至此,全书已看完一半了……

第八章 数据处理的两个基本问题

  1. 8086 CPU中,只有bx,si,di,dp四个寄存器才能用于内存寻址。并且,对于两个两个寄存器两两组合的情况,只有bx,bp 与 si,di 分别两两组合形成的4种组合方案。
  2. 内存寻址方式在 本书 P164 中有很好的总结,这里不再赘述。
  3. 指定处理的数据长度的方法有二:
    1. 通过寄存器名指明数据的尺寸。 因为位数不同的寄存器无法相互操作。
    2. 通过 操作符 X ptr 的方式指定。其中 byte ptr 指的是一个字节单位;word ptr指的是一个字单位; dword ptr 指的是一个双字单位。
  4. 在C语言中,我们经常可以看到,如:dec.cp[i]代表的是名为 dec的结构体中首地址为cp的变量中的第i个单元。而我们在汇编中做法存在bx.10h[si]。仔细一想的话,C语言中包含了很多与汇编相通的语法。只不过包了一层外衣。
  5. div指令。语法是div 除数(reg/内存单元),而我们的被除数一般优先放在 ax 中,其次放在 dx中。其商也是优先放在 ax 中,其次放在 dx 中。
  6. 伪指令 db,dw,dd。以下是三个伪指令的缩写,看了英文就会明白它的意思。
    • db: define byte
    • dw: define word
    • dd: define dword
  7. 指令操作符 dup。表示重复定义元素。语法为 db/dw/dd n dup (元素) 其中n为重复次数。在()为初值。比如 db 4 dup (0) 等同于 db 0,0,0,0

第九章 指令转移的原理

  1. 指令操作符 offset 。 offset 是偏移的意思。它的指令含义是获得标号的偏移地址。语法:offset 标号
  2. 指令操作符 nop 。 申请一个什么都没有的字节单位的机器码。
  3. cpu在执行jmp指令的时候无须知道目标地址。jmp指令只考虑与当前指令地址的偏移量。、
  4. jmp 语法小节:
    • jmp short 标号,偏移量在有符号字节单元内的段内转移。范围即[-128,127]
    • jmp near ptr 标号,偏移量在有符号字单元内的段内转移。范围即[-32768,32767]
    • jmp far ptr 标号,段间转移。用标号处的SA,EA来修改CS,IP。
    • jmp 16位 reg,用reg中的值来改变 ip 。
    • jmp word ptr 内存单元,段内转移。内存单元中存的是EA。
    • jmp dword ptr 内存单元,段间转移。内存单元中高位字存SA,低位字存EA。
  5. jcxz 标号。含义为 “jmp if cx equal to zero”。等同于C语句if((cx)==0) jmp short 标号;
  6. 补充,loop本质上也是有jmp指令而来,loop指令跳转的也是短转移。即其jmp范围为[-128,127]

第十章 call与ret指令

  1. ret 指令: 等同于 pop ip
  2. retf 指令: 等同于 pop ip pop cs指令。
  3. call 指令: 指令将进行两步操作,先将当前 ip 或者是 cs和ip 压入栈中,再是转移。其语法有
    1. call 标号:段内转移,等同于
      1. push ip
      2. jmp near ptr 标号
    2. call far ptr 标号:段间转移,等同于
      1. push cs
      2. push ip
      3. jmp far ptr 标号
    3. call 16位reg:段内转移,用寄存器中的数据进行转移,等同于
      1. push ip
      2. jmp 16位寄存器
    4. call word ptr 内存单元:段内转移,用内存数据进行转移,等同于
      1. push ip
      2. jmp word ptr 内存单元地址
    5. call dword ptr 内存单元:段间转移,用内存数据进行转移,等同于
      1. push cs
      2. push ip
      3. jmp dword ptr 内存单元
  4. 仔细观察一下,将call与ret结合起来使用就是一个函数的功能。
  5. 批量参数数据的传递可以通过申请内存并保存首地址的方式来实现。
  6. 指令操作符 mul:两个相乘的数,要么是8位,要么是16位,默认都放在ax寄存器中。另一个数字放在其他16位寄存器或者内存单元中。而其结果优先放在 ax 中,其次放在 dx 中。
  7. 寄存器是有限的,在使用call之后应首先把当前使用的寄存器的值预先放在栈中,在ret之前把栈的数据还原。

第十一章 标志寄存器

标志寄存器是8086的最后一个寄存器。也称为flag寄存器。flag寄存器是按位起作用的。其有效位如下

1514131211109876543210
OFDFIFTFSFZFAFPFCF

从低位到高位其各自英文全称与作用如下

  • CF: Carry Flag 。进位标志。在无符号计算时如果存在进位则为 1 。否则为 0 。
  • PF: Parity Flag 。奇偶标志。指令计算结果为偶数时为 1, 否则为 0 。
  • AF: Auxiliar Carry Flag。辅助进位标志。 意义不明。
  • ZF: Zero Flag。零标志。 指令结果为 0 则为 1。否则为 0。
  • SF: Sigh Flag。符号标志。 指令结果为 负数 则为 1。否则为 0 。
  • TF: Trace Flag。轨迹标志。 当TF为 1 时,执行完当前指令后将引起单步中断。
  • IF: Interrupt Flag。中断标志。 功能与 TF 一致。
  • DF: Direction Flag。方向标志。 DF为0时,每次指令操作后si,di递增,否则递减。
  • OF: Overflow Flag。溢出标志。在有符号计算时如果发生溢出,则为 1 ,否则为 0。

其他:

  1. adc 指令。带进位加法指令。adc ax,bx等同于 (ax)=(ax)+(bx)+CF,一般用于大数操作。
  2. sbb 指令。带借位减法指令。与adc指令类似。
  3. cmp 指令。cmp指令的功能类似于 减法指令。cmp指令虽然不会保存结果,但是会影响flag寄存器。通过flag寄存器的值,来判断两个数之间的大小关系。
  4. 以ax,bx为例,以下为无符号数大小比较之法:
    • $ ZF = 1 \rightarrow (ax) = (bx) $
    • $ ZF = 0 \rightarrow (ax) \neq (bx) $
    • $ CF = 1 \rightarrow (ax) < (bx) $
    • $ CF = 0 \rightarrow (ax) \geq (bx) $
    • $ CF = 0 \&\& ZF = 0 \rightarrow (ax) > (bx) $
    • $ CF = 1 \&\& ZF = 1 \rightarrow (ax) \leq (bx) $
  5. 以ax,bx为例,以下为有符号数大小比较之法:
    • $ ZF = 1 \rightarrow (ax) = (bx) $
    • $ ZF = 0 \rightarrow (ax) \neq (bx) $
    • $ SF ^ OF = 1 \rightarrow (ax) < (bx) $
    • $ SF = 0 \&\& OF = 0 \rightarrow (ax) \geq (bx) $
    • $ SF = 1 \&\& OF = 1 \rightarrow (ax) > (bx) $
  6. pushf和popf。将flag寄存器压入栈中或者从栈中弹出数据存到flag寄存器中。
  7. movsb / movsw 指令。将 ds:si 指向的内存单元的数据(字节/字)送入 es :di中,然后根据flag寄存器中的df位的值,将si,di递增或递减(1位/2位)。
  8. cld 指令将flag寄存器中的df位置变为 0,std指令则为置1 。
  9. 基于flag寄存器的条件转移,见表1。
  10. dos的debug中对于flag寄存器的表示,见表2。

表1

指令含义检测的相关标志位
je等于则转移$ ZF = 1 $
jne不等于则转移$ ZF = 0 $
jb低于则转移$CF =1 $
jnb不低于则转移$ CF = 0 $
ja高于则转移$ CF = 0 \&\& ZF = 0 $
jna不高于则转移$ CF = 1 || ZF = 1 $

表2

标志值为 1 的标记值为 0 的标志
OFOVNV
SFNGPL
ZFZRNZ
PFPEPO
CFCYNC
DFDNUP

第十二章 内中断

  1. 内存段有4种产生方式
    • 除法错误。中断类型码: 0
    • 单步执行。中断类型码: 1
    • 执行 into 指令。中断类型码: 4
    • 执行 int 指令。中断类型码: 其他
学习笔记

《操作系统真象还原》的一些笔记

Posted on

终于有时间开始搞我的OS Demo了。
希望能在元旦之前搞完。
这里不定时更新记录我在《操作系统真象还原》的一些笔记。以此督促和勉励自己。
很早之前就想看这本书了,之前在一个腾讯员工的书单上看到的,虽然貌似没什么名气,但看了一部分觉得还是挺有趣的,是我能非常乐意看下去的类型。那种带着程序员的幽默而不乏真实技术的书籍。

2017-10-24 23:50:14 星期二

  1. 编译器提供库函数,库函数封装了系统调用,这样的代码集合称之为运行库。
  2. 用户进程永远不会因为进入了内核态而变身为操作系统。
  3. cs: ip。 汇编指令,表示当前执行的指令。cs是代码段寄存器,ip是指令指针寄存器,指令指针计算为 $ cs \times 16 + ip $
  4. DRAM,动态随机访问内存。物理内存,也就是内存条就属于DRAM。其动态并不是内容变化的意思,而是保存时间短,需要顶起的刷新。
  5. 地址总线宽度决定可以访问的内存空间容量。书中所说是决定内存空间大小,我觉得应该是容量,或者说是上限,更为合适。yy 总线先分配外设地址,最后才将其余可用地址分配给DRAM。所以说,内存空间存在一个上限。
  6. 在8086中,内存空间中最前面的 1KB 地址为中断向量表。可以通过使用 int 中断号来实现相关的系统调用。
  7. 魔数。约定的含有具体意义但不说明的数字。在Linux中就是通过魔数来辨别文件系统。
  8. 尽管所有语言都会被转化成机器码,但中间存在效率,也许一个意思,Java语句有100条机器码,而汇编只有10条不到。
  9. 编译器的自我进化真的是非常神奇。内容太多,以后有机会可以再读p16。

2017-10-26 00:46:57 星期四

  1. CHS方式中扇区的编号是从1开始的
  2. 判别是否是主引导记录mbr的方式为文件末尾的两个魔数0x55和0xaa
  3. mbr固定会被加载到内存的 0x7c00 地址
  4. \$ 是编译器给当前行安排的地址,\$\$则是本section的起始地址
  5. section 只是用于给程序员在逻辑上规划代码用的,并没有什么实质意义
  6. 编译器有一个重要的工作是给程序中各符号编址。很重要,符号包括数据类型,数据。编译器只负责编址,它只会将数据相对于文件开头的偏移量作为该数据的地址。
  7. vstart。虚拟起始地址。告知编译器在当前section之后的所有数据都编在这个vstart地址之后。用vstart的时机是我预先知道我的程序将被加载在某处。
  8. 数据的内存地址与文件地址应严格区分开来。数据的文件地址不会受到vstart影响。它的地址绝大多数是连续的。目前的个人理解,可能有错误

2017-10-27 00:09:24 星期五

麻辣格鸡的好难

  1. 实模式。是指CPU的寻址方式,寄存器大小,指令用法等,用来反应CPU在该环境下如何工作的概念。
  2. CPU唯一的任务就是执行指令。
  3. CPU大体分为三个部分,控制单元,运算单元,存储单元。
  4. 控制单元大致由指令寄存器,指令译码器,操作控制器组成。功能很好理解,先存储指令,再解码指令,最后进行操作,对其他CPU部分开始控制。
  5. 很多地方都用到了缓存,浏览器的访问就是一个简单例子。
  6. 寄存器之所以快,是由于它有触发器实现。已经完全忘了
  7. CPU的寄存器可分为对程序员不可见与可见。比如一些固定数据的存储,肯定是不能让程序员访问的。
  8. 实模式下,默认用到的寄存器都是16位宽。
  9. flags寄存器是计算机的窗口,展示了CPU内部的各项设置,指标。并不知道怎么用
  10. 八个通用寄存器 AX BX CX DX SI DI BP SP,其中前四个又可各自划分为两个部分。比如对于 AX来说,可以划分为 高位(High) AH ,低位(Low) AL。也可扩展(Extend) 为 EAX
  11. 是历史上第一款 x86 CPU,在8086之前都是不存在段的概念,直接用硬编码访问内存。
  12. 8086通过“段+偏移”来实现20位寻址,16 + 16 - 20,多出的 12 位不用管它,会自动进行取模运算。

2017-10-28 01:00:32 星期六

先学汇编去了 传送门