只有了解底层硬件的工作原理,才能理解操作系统的工作模式。
1. 8086 架构和操作系统相关的部分
虽然 8086 处理器已经很老了,但是现在操作系统很多特性都和它有关,并且一直保持兼容。如下图所示,是 CPU 里面的组件放大之后的情况。
下面对 8086 架构中的一些内容进行讲解:
-
8086 处理器内部有 8 个 16 位的通用寄存器,也就是 CPU 内部的数据单元,分别是 AX、BX、CX、DX、SP、BP、SI、DI,这些寄存器的作用主要是暂存计算机过程中的数据。
另外,AX、BX、CX、DX 这四个寄存器又可以分为两个 8 位的寄存器来使用,分别是 AH、AL、BH、BL、CH、CL、DH、DL,其中 H 表示高位(high),L 表示低位(low)的意思。
需要 8 位,是因为那时候是计算器刚刚起步。
-
下面来看下控制单元。
IP 寄存器就是指令指针寄存器(Instruction Pointer Register),指向代码段中下一条指令的位置。CPU 会根据它不断地从内存的代码段中取出指令并加载到 CPU 的指令队列中,然后交给运算单元去执行。
CS、DS、SS、ES 这四个寄存器都是 16 位寄存器,用来存储进程的地址空间信息。比如,
- CS 是代码段寄存器(Code Segment Register),通过它可以找到代码在内存中的位置;
- DS 是数据段寄存器(Data Segment Register),通过它可以找到数据在内存中的位置;
- SS 是栈寄存器(Stack Register),栈是程序运行过程所需要的一种数据结构,主要用于记录函数调用的关系;
- ES 是一个附加段寄存器(Extra Segment Register),当发现段寄存器不够用的时候,你可以考虑使用 ES 段寄存器。
那么如何根据上述段寄存器找到所需的地址?CS 和 DS 中都存放着一个段的起始地址,代码段的偏移值存放在 IP 寄存器中,而数据段的偏移值放在通用寄存器中。由于 8086 架构中总线地址是 20 位的,而段寄存器和 IP 寄存器以及通用寄存器都是 16 位的,所以为了得到 20 位的地址,先将段寄存器中起始地址左移 4 位,然后再加上偏移量,就得到了 20 位的地址。由于偏移量是 16 位的,所以每个段最大的大小是 64 K 的。
另外,对于 20 位的地址总线来说,能访问到的内存大小最多也就只有 2^20 = 1 MB。如果计算得到某个要访问的地址是 1MB+X,那么最后访问的是地址 X,因为地址线只能发送低 20 位的。
2. 80386 架构(32 位)和操作系统相关的部分
为了使得运行在 8086 架构上的程序在移到 32 位架构之后也能执行,那么 32 位的架构需要兼容 8086 架构的,那么怎么保持兼容呢?
-
通用寄存器从 16 位变成了 32 位,也就是 8 个 32 位的通用寄存器。但是为了保持兼容,仍然保留了 16 位和 8 位的使用方式,如果所示的 AH、AL 等。
-
指向下一条指令的指令指针寄存器也从 16 位变成了 32 位的,被称为 EIP,但是同样兼容 16 位的使用方式。
-
段寄存器改动比较大。32 位中段寄存器还是 16 位,但是它不再表示段的起始地址,而是表示索引。32 位架构中,引入了段描述符表,表格中的每一项都是段描述符(Segment Descriptor),记录了段在内存中的起始位置,而这张表则存放在内存的某个地址。那么,段寄存器中存的就是对应段在段表中的位置,称为选择子(selector)。
先根据段寄存器拿到段的起始地址,再根据段寄存器中保存的选择子,找到对应的段描述符,然后从这个段描述符中取出这个段的起始地址。就相当于直接找到段起始地址变成了间接找到段起始地址。这样改变之后,段起始地址会变得很灵活。
但是这样就跟原来的 8086 架构不兼容了,因此为了兼容 8086 架构,32 位架构中引入了实模式和保存模式,8086 架构中的方式就称为实模式,32 位这种模式就被称为保护模式。当系统刚刚启动的时候,CPU 是处于实模式的,这个时候和 8086 模式是兼容的。当需要更多内存时,进行一系列的操作,将其切换到保护模式,这样就能使用 32 位了。
模式可以理解为,CPU 和操作系统的一起干活的模式,在实模式下,两者约定好了这些寄存器是干这个的,总线是这样的,内存访问是这样的,在保护模式下,两者约定好了这些寄存器是干那个的,总线是那样的,内存访问是那样的。这样操作系统给CPU下命令,CPU按照约定好的,就能得到操作系统预料的结果,操作系统也按照约定好的,将一些数据结构,例如段描述符表放在一个约定好的地方,这样CPU就能找到。两者就可以配合工作了。
3. 64 位架构和操作系统相关的部分
16 个 64 位通用寄存器:%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp, %r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。这些寄存器虽然都可以用,但是为了方便软件编写还是做了一些规定,如下:
-
函数返回值存放的寄存器:rax
-
栈指针寄存器:rsp
-
给函数传整型参数的寄存器:rdi、rsi、rdx、rcx、r8、r9
-
rbx、rbp、r12、r13、r14、r15:这些寄存器由被调用者负责保护,在返回的时候要恢复这些寄存器中原本的值。
上述的寄存器名字都是 64 位的名字,对于每个寄存器,我们还可以只使用它的一部分,并使用另一个新的名字。
下面这些寄存器可能也会需要用到其他寄存器:
- 8 个 80 位的 x87 寄存器,用于浮点计算
- 8 个 64 位的 MMX 寄存器,用于 MMX 指令(多媒体指令),这 8 个寄存器跟 x87 寄存器在物理上是相同的寄存器。
- 16 个 128 位 的 SSE 寄存器,用于 SSE 指令
- RIP 指令寄存器,保存指令地址。
- flags (rflags-64 位,eflags-32 位)寄存器。每个位用来标识一个状态。比如,这些标识符可能用于比较和跳转的指令。
注意:64 位也有实模式。
4. 常用的指令
指令 | 简单解释 | 解释 |
---|---|---|
mov obj source | 把 source 赋值给 obj | load value. this instruction name is misnomer, resulting in some confusion (data is not movedbut copied), in other architectures the same instructions is usually named “LOAD” and/or “STORE”or something like that.One important thing: if you set the low 16-bit part of a 32-bit register in 32-bit mode, the high 16bits remains as they were. But if you modify the low 32-bit part of the register in 64-bit mode, thehigh 32 bits of the register will be cleared.Supposedly, it was done to simplify porting code to x86-64. |
call | 调用子程序 | call another function:PUSH address_after_CALL_instruction; JMP label. |
ret | 子程序以 ret 结尾 | return from subroutine:POP tmp; JMP tmp. In fact, RET is an assembly language macro, in Windows and *NIX environment it is translated into RETN (“return near”) or, in MS-DOS times, where the memory was addressed differently, into RETF (“return far”).RET can have an operand. Then it works like this:POP tmp; ADD ESP op1; JMP tmp.RETwith an operand usually ends functions in the stdcall calling convention. |
jmp | 无条件跳 | jump to another address. The opcode has ajump offset. |
int | 中断指令 | INT x is analogous to PUSHF; CALL dword ptr [x*4]in 16-bit environment. It was widely used in MS-DOS, functioning as a syscall vector. The registers AX/BX/CX/DX/SI/DI were filled with the arguments and then the flow jumped to the address in the Interrupt Vector Table (located at thebeginning of the address space). It was popular because INT has a short opcode (2 bytes) and the program which needs some MS-DOS services is not bother to determine the address of the service’sentry point. The interrupt handler returns the control flow to caller using the IRET instruction.The most busy MS-DOS interrupt number was 0x21, serving a huge part of itsAPI. In the post-MS-DOS era, this instruction was still used as syscall both in Linux and Windows (6.3 onpage 750), but was later replaced by the SYSENTER or SYSCALL instructions. |
add | 加法,a=a+b | add two values. |
or | 或运算 | logical “or”. |
xor | 异或运算 | is in fact just “eXclusive OR”,but the compilers often use it instead of MOV EAX, 0—again becauseit is a slightly shorter opcode (2 bytes for XOR against 5 for MOV). |
shl | 逻辑左移 | shift value left/right. Bit shifts in C/C++ are implemented using≪and≫operators. The x86 ISA has the SHL (SHift Left) and SHR (SHift Right) instructions for this. Shift instructions are often used in division and multiplications bypowers of two:2n(e.g., 1, 2, 4, 8, etc.): Shifting operations are also so important because they are often used for specific bit isolation or forconstructing a value of several scattered bits. |
ahr | 逻辑右移 | |
push xxx | 压xxx入栈 | push a value into the stack:ESP=ESP-4 (or 8); SS:[ESP]=value. |
pop xxx | xxx出栈 | get a value from the stack:value=SS:[ESP]; ESP=ESP+4 (or 8). The most frequently used stack access instructions are PUSH and POP(in both x86 and ARM Thumb-mode).PUSH subtracts from ESP/RSP/SP4 in 32-bit mode (or 8 in 64-bit mode) and then writes the contents of its sole operand to the memory address pointed by ESP/RSP/SP. POP is the reverse operation: retrieve the data from the memory location that SP points to, load it into the instruction operand (often a register) and then add 4 (or 8) to the stack pointer. |
inc | 加一 | increment. Unlike other arithmetic instructions,INC doesn’t modify CF flag. |
dec | 减一 | decrement. Unlike other arithmetic instructions,DEC doesn’t modify CF flag. |
sub a b | a=a-b | subtract values. A frequently occurring pattern is SUB reg. |
cmp ax,bx | 减法比较,修改标志位 | cmp 的功能相当于减法指令,只是不保存结果。cmp 指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。如果执行后,ZF=1 则 AX=BX;ZF=0 则 AX!=BX;SF=1 则 AX<BX;SF=0 则 AX>=BX;SF=0 并 ZF=0 则 AX>BX;SF=1 或 ZF=1 则 AX<=BX。 |
8086CPU设置了一个16位标志寄存器PSW(也叫FR),其中规定了 9 个标志位,用来存放运算结果特征和控制 CPU 操作。
9个标志位可以分为两类:
条件码:
①OF(Overflow Flag)溢出标志,溢出时为1,否则置0.标明一个溢出了的计算,如:结构和目标不匹配.
②SF(Sign Flag)符号标志,结果为负时置1,否则置0.
③ZF(Zero Flag)零标志,运算结果为0时置1,否则置0.
④CF(Carry Flag)进位标志,进位时置1,否则置0.注意:Carry标志中存放计算后最右的位.
⑤AF(Auxiliary carry Flag)辅助进位标志,记录运算时第3位(半个字节)产生的进位置。
有进位时1,否则置0.
⑥PF(Parity Flag)奇偶标志.结果操作数中1的个数为偶数时置1,否则置0.控制标志位:
⑦DF(Direction Flag)方向标志,在串处理指令中控制信息的方向。
⑧IF(Interrupt Flag)中断标志。
⑨TF(Trap Flag)陷井标志。
5. x86 相关资料
推荐一个入门的系统学习汇编的视频课,网易云课堂上的一个课程,《汇编从零开始到C语言》
Guide to x86 Assembly: http://www.cs.virginia.edu/~evans/cs216/guides/x86.html
大部分pc机都是用的x86架构的CPU,为什么会形成这样的格局?建议看看吴军老师的《浪潮之巅》上册第5章
6. 巨人的肩膀
- 《趣学Linux操作系统》 .极客时间
- https://blog.csdn.net/weixin_45468845/article/details/108856610