Linux0.11 80X86知识(七)(上)

简介: Linux0.11 80X86知识(七)

一、C 与汇编程序的相互调用

1、C 函数调用机制

 函数调用操作包括从一块代码到另一块代码之间的双向数据传递和执行控制转移。数据传递通过函数参数和返回值来进行。另外,我们还需要在进入函数时为函数的局部变量分配存储空间,并且在退出函数时收回这部分空间。Intel 80x86 CPU 为控制传递提供了简单的指令,而数据的传递和局部变量存储空间的分配与回收则通过栈操作来实现。

1.1 栈帧结构和控制转移权方式

  大多数 CPU 上的程序实现使用栈来支持函数周用操作。栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用来存储局部数据。单个函数调用操作所使用的栈部分被称为栈帧(Stack frame)结构,其通常结构见下图所示。栈帧结构的两端由两个指针来指定。寄存器 ebp 通常用作帧指针(frame pointer),而 esp 则用作栈指针(stack pointer)。在函数执行过程中,栈指针 esp 会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于帧指针 ebp 进行。

 对于函数 A 调用函数 B 的情况,传递给 B 的参数包含在 A 的栈帧中。当 A 调用 B 时,函数 A 的返回地址(调用返回后继续执行的指令地址)被压入栈中,栈中该位置也明确指明了 A 栈帧的结束处。而 B 的栈帧则从随后的栈部分开始,即图中 保存帧指针(ebp) 的地方开始。再随后则用于存放任何保存的寄存器值以及函数的临时值。

 B 函数同样也使用栈来保存不能放在寄存器中的局部变量值。例如由于通常 CPU 的寄存器数量有限而不能够存放函数的所有局部数据,或者有些局部变量是数组或结构,因此必须使用数组或结构引用来访问。还有就是 C 语言的地址操作符 ‘&’ 被应用到一个局部变量上时,我们就需要为该变量生成一个地址,即为变量的地址指针分配一空间。最后,B 函数会使用栈来保存调用任何其它函数的参数。

 栈是往低(小)地址方向扩展的,而 esp 指向当前栈顶处的元素。通过使用 push 和 pop 指令我们可以把数据压入栈中或从栈中弹出。对于没有指定初始值的数据所需要的存储空间,我们可以通过把栈指针递减适当的值来做到。类似地,通过增加栈指针值我了可以回收栈中已分配的空间。

 指令 CALL 和 RET 用于处理函数调用和返回操作。调用指令 CALL 的作用是把 返回地址 压入栈中并且跳转到被调用函数开始处执行。返回地址 是程序中紧随调用指令 CALL 后面一条指令的地址。因此当被调函数返回时就会从该位置继续执行。返回指令 RET 用于弹出栈项处的地址并跳转到该地址处。在使用该指令之前,应该先正确处理栈中内容,使得当前栈指针所指位置内容正是先前 CALL 指令保存的返回地址。另外,若返回值是一个整数或一个指针,那么寄存器 eax 将被默认用来传递返回值。

 尽管某一时刻只有一个函数在执行,但我们还是需要确定在一个函数(调用者)调用其他函数(被调用者)时,被调用者不会修改或覆盖掉调用者今后要用到的寄存器内容。因此 Intel CPU 采用了所有函数必须遵守的 寄存器用法统一惯例 。该惯例指明,寄存器 eax、edx 和 ecx 的内容必须由调用者自己负责保存当函数 B 被 A 调用时,函数 B 可以在不用保存这些寄存器内容的情况下任意使用它们而不会毁坏函数 A 所需要的任何数据。另外,寄存器 ebx、esi 和 edi 的内容则必须由被调用者 B 来保护。当被调用者需要使用这些寄存器中的任意一个时,必须首先在栈中保存其内容,并在退出时恢复这些寄存器的内容。因为调用者 A (或者一些更高层的函数)并不负责保存这些寄存器内容,但可能在以后的操作中还需要用到原先的值。还有寄存器 ebp 和 esp 也必须遵守第二个惯例用法。

1.2 函数调用举例

  作为一个例子,我们来观察下面 C 程序 exch.c 中函数调用的处理过程。该程序交换两个变量中的值,并返回它们的差值。

void swap(int* a, int* b) {
  int c;
  c = *a; 
  *a = *b;
  *b = c;
}

int main() {
  int a, b;
  a = 16;
  b = 32;
  swap(&a, &b);
  return (a - b);
}

  其中函数 swap() 用于交换两个变量的值。C 程序中的主程序 main() 也是一个函数(将在下面说明),它在调用了 swap() 之后返回交换后的结果。这两个函数的栈帧结构如下图所示。可以看出,函数 swap() 从调用者( main() )的栈帧中获取其参数。图中的位置信息相对于寄存器 ebp 中的帧指针。栈帧左边的数字指出了相对于帧指针的地址偏移值。在像 gdb 这样的调试器中,这些数值都用 2 的补码表示。例如 ‘-4’ 被表示成 ‘0xFFFFFFFC’ ,‘-12’ 会被表示成’0xFFFFFFF4’。

  调用者 main() 的栈帧结构中包括局部变量 ab 的存储空间,相对于帧指针位于 -4-8 偏移处。由于我们需要为这两个局部变量生成地址,因此它们必须保存在栈中而非简单地存放在寄存器中。


  使用命令 “gcc -Wall -S -o exch.s exch.c” 可以生成该 C 语言程序的汇编程序 exch.s 代码,见如下所示。

 1  _.text
 2  _swap:
 3    pushl %ebp          #保存原 ebp 值,设置当前函数的帧指针。
 4    movl %esp,%ebp
 5    subl $4,%esp        #为局部变量 c 在栈内分配空间。
 6    movl 8(%ebp),%eax     #取函数第 1 个参数,该参数是一个整数类型值的指针。
 7    movl(%eax),%ecx       #取该指针所指位置的内容,并保存到局部变量 c 中。
 8    movl %ecx, -4(%ebp)
 9    movl 8(%ebp),%eax     # 再次取第 1 个参数,然后取第 2 个参数。
10    movl 12(%ebp),%edx
11    movl (%edx),%ecx      # 把第 2 个参数所指内容放到第 1 个参数所指的位置。
12    movl %ecx, (%eax)
13    movl 12(%ebp),%eax      # 再次取第 2 个参数。
14    movl -4(%ebp),%ecx      #然后把局部变量 c 中的内容放到这个指针所指位置处。
15    movl %ecx,(%eax)
16    leave           # 恢复原 ebp、esp 值(即 movl %ebp,%esp; popl %ebp;)。
17    ret
18  _main:
19    pushl %ebp          # 保存原 ebp 值,设置当前函数的帧指针。
20    movl %esp,%ebp
21    subl $8,%esp        #为整型局部变量 a 和 b 在栈中分配空间。
22    movl $16,-4(%ebp)     #为局部变量赋初值(a=16,b=32)。
23    mov1 $32,-8(%ebp)
24    leal -8(%ebp),%eax      # 为调用 swap ()函数作准备,取局部变量 b 的地址,
25    pushl %eax          # 作为调用的参数并压入栈中。即先压入第 2 个参数。
26    leal -4(%ebp),%eax      # 再取局部变量 a 的地址,作为第 1 个参数入栈。
27    push1 %eax
28    call _swap          # 调用函数 swap()。
29    movl -4(%ebp),%eax      #取第 1 个局部变量 a 的值,减去第 2 个变量 b 的值。
30    subl -8(%ebp),%eax
31    leave           #恢复原 ebp、esp 值(即 ovl %ebp,%esp; popl %ebp;)。
32    ret

这两个函数均可以划分成三个部分:

  • “设置”,初始化栈帧结构;
  • “主体”,执行函数的实际计算操作;
  • “结束”,恢复栈状态并从函数中返回。

 对于 swap() 函数,其设置部分代码是 3–5 行。前两行用来设置保存调用者的帧指针和设置本函数的栈帧指针,第 5 行通过把栈指针 esp 下移 4 字节为局部变量 c 分配空间。行 6–15 是 swap 函数的主体部分。第 6–8 行用于取调用者的第 1 个参数&a,并以该参数作为地址取所存内容到 ecx 寄存器中,然后保存到为局部变量分配的空间中(-4(%ebp))。第 9–12 行用于取第 2 个参数&b,并以该参数值作为地址取其内容放到第 1 个参数指定的地址处。第 13–15 行把保存在临时局部变量 c 中的值存放到第 2 个参数指定的地址处。最后 16-17 行是函数结束部分。leave 指令用于处理栈内容以准备返回,它的作用等价于下面两个指令:

mov1 %ebp,%esp    # 恢复原 esp 的值(指向栈帧开始处)-
popl %ebp     # 恢复原 ebp 的值(通常是调用者的帧指针)。

这部分代码恢复了在进入 swap() 函数时寄存器 esp 和 ebp 的原有值,并执行返回指令 ret。

 第 19–21 行是 main()函数的设置部分,在保存和重新设置帧指针之后,main()为局部变量 a 和 b 在栈中分配了空间。第 22–23 行为这两个局部变量赋值。从 24-28 行可以看出 main() 中是如何调用 swap() 函数的。其中首先使用 leal 指令(取有效地址)获得变量 b 和 a 的地址并分别压入栈中,然后调用 swap() 函数。变量地址压入栈中的顺序正好与函数申明的参数顺序相反。即函数最后一个参数首先压入栈中,而函数的第 1 个参数则是最后一个在调用函数指令 call 之前压入栈中的。第 29–30 两行将两个已经交换过的数字相减,并放在 eax 寄存器中作为返回值。

 从以上分析可知,C 语言在调用函数时是在堆栈上临时存放被调函数参数的值,即 C 语言是传值类语言,没有直接的方法可用来在被调用函数中修改调用者变量的值。因此为了达到修改的目的就需要向函数传递变量的指针(即变量的地址)。

1.3 main() 也是一个函数

 上面这段汇编程序是使用 gcc 1.40 编译产生的,可以看出其中有几行多余的代码。可见当时的 gcc 编译器还不能产生最高效率的代码,这也是为什么某些关键代码需要直接使用汇编语言编制的原因之一。另外,上面提到 C 程序的主程序 main()也是一个函数。这是因为在编译链接时它将会作为 crt0.s 汇编程序的函数被调用。crt0.s 是一个桩(stub)程序,名称中的"crt"是"C run-time"的缩写。该程序的目标文件将被链接在每个用户执行程序的开始部分,主要用于设置一些初始化全局变量等。Linux 0.11 中 crt0.s 汇编程序见如下所示。其中建立并初始化全局变量 _environ 供程序中其它模块使用。

 1  .text
 2  .globl _environ       #声明全局变量 _environ(对应 C 程序中的 environ 变量)。
 3  
 4  _entry:           #代码入口标号。
 5    movl 8(%esp), %eax    # 取程序的环境变量指针 envp 并保存在_environ 中。
 6    movl %eax, _environ   # envp 是 execveO函数在加载执行文件时设置的。
 7    call _main        # 调用我们的主程序。其返回状态值在 eax 寄存器中。
 8    pushl %eax        # 压入返回值作为 exit()函数的参数并调用该函数。
 9  1:  call _exit        
10    _jmp 1b         #控制应该不会到达这里。若到达这里则继续执行 exit()。
11  .data
12  _environ:         # 定义变量_environ,为其分配一个长字空间。
13    .long 0

2、汇编中调用 C 函数

 从汇编程序中调用 C 语言函数的方法实际上在上面已经给出。在上面 C 语言例子对应的汇编程序代码中,我们可以看出汇编程序语句是如何调用 swap() 函数的。现在我们对调用方法作一总结。

 在汇编程序调用一个 C 函数时,程序需要首先按照逆向顺序把函数参数压入栈中,即函数最后(最右边的)一个参数先入栈,而最左边的第 1 个参数在最后调用指令之前入栈,见图 3-6 所示。然后执行 CALL指令去执行被调用的函数。在调用函数返回后,程序需要再把先前压入栈中的函数参数清除掉。

  在执行 CALL 指令时,CPU 会把 CALL 指令下一条指令的地址压入栈中(见图中 EIP )。如果调用还涉及到代码特权级变化,那么 CPU 还会进行堆栈切换,并且把当前堆栈指针、段描述符和调用参数压入新堆栈中由于 Linux 内核中只使用中断门和陷阱门方式处理特权级变化时的调用情况,并没有使用 CALL指令来处理特权级变化的情况,因此这里对特权级变化时的 CALL 指令使用方式不再进行说明。

 汇编中调用 C 函数比较"自由"。只要是在栈中适当位置的内容就都可以作为参数供 C 函数使用。这里仍然以图 3-6 中具有 3 个参数的函数调用为例,如果我们没有专门为调用函数 func()压入参数就直接调用它的话,那么 func()函数仍然会把存放 EIP 位置以上的栈中其他内容作为自己的参数使用。 如果我们为调用 func()而仅仅明确地压入了第 1、第 2 个参数,那么 func()函数的第 3 个参数 p3 就会直接使用 p2 前的栈中内容。在 Linux 0.1x 内核代码中就有几处使用了这种方式。例如在 kernel/systemm_call.s 汇编程序中第 217 行上调用 copy_process()函数(kernel/fork.c 中第 68 行)的情况。在汇编程序函数_sys_fork中虽然只把 5 个参数压入了栈中,但是 copy_process()却共带有多达 17 个参数。

3、C 中调用汇编函数

查看前面的_swap ,就是一个汇编函数:

 1  _.text
 2  _swap:
 3    pushl %ebp          #保存原 ebp 值,设置当前函数的帧指针。
 4    movl %esp,%ebp
 5    subl $4,%esp        #为局部变量 c 在栈内分配空间。
 6    movl 8(%ebp),%eax     #取函数第 1 个参数,该参数是一个整数类型值的指针。
 7    movl(%eax),%ecx       #取该指针所指位置的内容,并保存到局部变量 c 中。
 8    movl %ecx, -4(%ebp)
 9    movl 8(%ebp),%eax     # 再次取第 1 个参数,然后取第 2 个参数。
10    movl 12(%ebp),%edx
11    movl (%edx),%ecx      # 把第 2 个参数所指内容放到第 1 个参数所指的位置。
12    movl %ecx, (%eax)
13    movl 12(%ebp),%eax      # 再次取第 2 个参数。
14    movl -4(%ebp),%ecx      #然后把局部变量 c 中的内容放到这个指针所指位置处。
15    movl %ecx,(%eax)
16    leave           # 恢复原 ebp、esp 值(即 movl %ebp,%esp; popl %ebp;)。
17    ret

在 C 语言中调用示例如下:

int main() {
  int a, b;
  a = 16;
  b = 32;
  swap(&a, &b);
  return (a - b);
}


4、查看汇编代码

linux查看C程序的汇编代码(可读懂)

vscode反汇编以及调试

方法 一:使用gcc -S a.c得到a.s,用cat a.s查看;

方法二:先gcc -c a.c得到a.o,用objdump -d a.o来反汇编查看里面的汇编代码;

方法三:在调试中进入gdb, 用disassemble命令查看。


objdump有多个命令选项,可根据需要选择:

-d:将代码段反汇编

-S:将代码段反汇编的同时,将反汇编代码和源代码交替显示,编译时需要给出-g,即需要调试信息。

-C:将C++符号名逆向解析。

-l:反汇编代码中插入源代码的文件名和行号。

-j section:仅反汇编指定的section。可以有多个-j参数来选择多个section。

vscode 中

打断点 F5调试后:

(1)查看汇编代码。在调试控制台输入

-exec disassemble /m 或者 -exec disassemble /m main

(2)查看寄存器的信息。在调试控制台输入

-exec info registers

5、函数返回值

#include <iostream>

int add(int a, int b) {
  auto sum = a + b;
  return sum;
}

int main(int, char **) {
  std::cout << "Hello, world!\n";
  int sum1 = add(1, 2);
  std::cout << "sum:" << sum1 << std::endl;
}

使用如下命令查看汇编代码:

objdump -d -S -C ./build/CMakeFiles/c6.dir/main.cpp.o

汇编代码:

0000000000000000 <add(int, int)>:
#include <iostream>

int add(int a, int b) {
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)
   7:   89 75 e8                mov    %esi,-0x18(%rbp)
  auto sum = a + b;
   a:   8b 55 ec                mov    -0x14(%rbp),%edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax
  10:   01 d0                   add    %edx,%eax
  12:   89 45 fc                mov    %eax,-0x4(%rbp)
  return sum;
  15:   8b 45 fc                mov    -0x4(%rbp),%eax
}
  18:   5d                      pop    %rbp
  19:   c3                      retq

000000000000001a <main>:

int main(int, char **) {
  1a:   55                      push   %rbp
  1b:   48 89 e5                mov    %rsp,%rbp
  1e:   48 83 ec 20             sub    $0x20,%rsp
  22:   89 7d ec                mov    %edi,-0x14(%rbp)
  25:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
  std::cout << "Hello, world!\n";
  29:   be 00 00 00 00          mov    $0x0,%esi
  2e:   bf 00 00 00 00          mov    $0x0,%edi
  33:   e8 00 00 00 00          callq  38 <main+0x1e>
  int sum1 = add(1, 2);
  38:   be 02 00 00 00          mov    $0x2,%esi
  3d:   bf 01 00 00 00          mov    $0x1,%edi
  42:   e8 00 00 00 00          callq  47 <main+0x2d>
  47:   89 45 fc                mov    %eax,-0x4(%rbp)
  std::cout << "sum:" << sum1 << std::endl;
  4a:   be 00 00 00 00          mov    $0x0,%esi
  4f:   bf 00 00 00 00          mov    $0x0,%edi
  54:   e8 00 00 00 00          callq  59 <main+0x3f>
  59:   48 89 c2                mov    %rax,%rdx
  5c:   8b 45 fc                mov    -0x4(%rbp),%eax
  5f:   89 c6                   mov    %eax,%esi
  61:   48 89 d7                mov    %rdx,%rdi
  64:   e8 00 00 00 00          callq  69 <main+0x4f>
  69:   be 00 00 00 00          mov    $0x0,%esi
  6e:   48 89 c7                mov    %rax,%rdi
  71:   e8 00 00 00 00          callq  76 <main+0x5c>
}
  76:   b8 00 00 00 00          mov    $0x0,%eax
  7b:   c9                      leaveq
  7c:   c3                      retq

二、AT&T汇编

C 语言与嵌入汇编 详细篇

8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp

8个16-bit寄存器 它们事实上是上面8个32-bit寄存器的低16位:
%ax,%bx,%cx,%dx,%di,%si,%bp,%sp

8个8-bit寄存器  %ah,%al,%bh,%bl,%ch,%cl,%dh,%dl
它们事实上是寄存器%ax,%bx,%cx,%dx的高8位和低8位

6个段寄存器   %cs(code),%ds(data),%ss(stack), %es,%fs,%gs

3个控制寄存器 %cr0,%cr2,%cr3; 

6个debug寄存器  %db0,%db1,%db2,%db3,%db6,%db7;

2个测试寄存器  %tr6,%tr7; 

8个浮点寄存器栈 %st(0),%st(1),%st(2),%st(3),%st(4),%st(5),%st(6),%st(7)

16个逻辑上的通用寄存器 rax, rbx, rcx, rdx, rbp, rsp, rdi, rsi, r8~r15


以E开头的寄存器为32位

EAX(累加器,是算术运算的主要寄存器)

EBX(基址寄存器,在内存中寻址时存放基址)

ECX(计数器)

EDX(数据寄存器)

ESI(源变址)

EDI(目标变址)

ESP(堆栈指针)

EBP(基址指针)

EIP(程序计数器,存储的是将要执行的下一条指令放在内存中的地址)

EFLAGS(保存的是根据运算得到的结果设置的条件码ZF,CF,SF,OF)


段寄存器:

CS:代码段寄存器

SS:堆栈段寄存器

DS:数据段寄存器

ES、FS、GS:附加数据段寄存器

image.png

image.png

1、GNUC C 语言嵌入汇编

2.2.1 GNU C内嵌汇编语言

Linux下C语言asm,linux下的C语言的asm内嵌式汇编

c语言关键字asm的用法,在 C语言中,如何通过 asm 关键字嵌入汇编语言代码

C语言中嵌入汇编

内嵌汇编(ARM64)

2、GNUC 汇编

x86汇编语法基础(gnu格式)

x86汇编_指令集大全_笔记_6

x86 Assembly Language Reference Manual(AT&T syntax)

AT&T汇编

AT&T的汇编世界

三、80X86

1、标志寄存器

    标志寄存器 EFLAGS 中的系统标志和 IOPL 字段用于控制 I/O 访问、可屏蔽硬件中断、调试、任务切换以及虚拟-8086 模式,见图 4-1 所示。通常只允许操作系统代码有权修改这些标志。EFLAGS 中的其他标志是一些通用标志进位 CF、奇偶 PF、辅助进位 AF、零标志 ZF、负号 SF、方向 DF、溢出 OF)。这里我们仅队 EFLAGS 中的系统标志进行说明。


TF

位 8 是跟踪标志(Trap Flag)。当设置该位时可为调试操作启动单步执行方式;复位时则禁止单步。执行。在单步执行方式下,处理器会在每个指令执行之后产生一个调试异常,这样我们就可以观察执行程序在执行每条指令后的状态。如果程序使用 POPF、POPFD 或 IRET 指令设置了 TF 标志,那么在随后指令之后处理器就会产生一个调试异常。

IOPL

位 13-12 是 I/O 特权级(I/O Privilege Level)字段。该字段指明当前运行程序或任务的 I/O 特权级 IOPL。当前运行程序或任务的 CPL 必须小于等于这个 IOPL 才能访问 I/O 地址空间。只有当 CPL 为特权级 0 时,程序才可以使用 POPF 或 IRET 指令修改这个字段。IOPL 也是控制对 IF 标志修改的机制之一。

NT

位 14 是嵌套任务标志(Nested Task)。它控制着被中断任务和调用任务之间的链接关系。在使用 CALL 指令、中断或异常执行任务调用时,处理器会设置该标志。在通过使用 IRET 指令从一个任务返回时,处理器会检查并修改这个 NT 标志。使用 POPF/POPFD 指令也可以修喂这个标志,但是在应用程序中改变这个标志的状态会产生不可意料的异常。

RF

位 16 是恢复标志(Resume Flag)。该标志用于控制处理器对断点指令的响应。当设置时,这个标志会临时禁止断点指令产生的调试异常;当该标志复位时,则断点指令将会产生异常。RF 标志的主要功能是允许在调试异常之后重新执行一条指令。当调试软件使用 IRETD 指令返回被中断程序之前,需要设置堆栈上 EFLAGS 内容中的 RF 标志,以防止指令断点造成另一个异常。处理器会在指令返回之后自动地清除该标志,从而再次允许指令断点异常。

VM

位 17 是虚拟-8086 方式(Virtual-8086 Mode)标志。当设置该标志时,就开启虚拟-8086 方式;当复位该标志时,则回到保护模式。

2、内存管理寄存器

    GDTR、LDTR、IDTR 和 TR 都是段基址寄存器,这些段中含有分段机制的重要信息表。

GDTR、IDTR 和 LDTR 用于寻址存放描述符表的段。TR 用于寻址一个特殊的任务状态段 TSS(Task State Segment)。TSS段中包含着当前执行任务的重要信息。

1.全局描述符表寄存器 GDTR

   GDTR 寄存器中用于存放全局描述符表 GDT 的 32 位线性基地址和 16 位表长度值。基地址指定 GDT 表中字节 0 在线性地址空间中的地址,表长度指明 GDT 表的字节长度值。指令 LGDT 和 SGDT 分别用于加载和保存 GDTR 寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为 0,而长度值被设置成 0xFFFF。在保护模式初始化过程中必须给 GDTR 加载一个新值。

2. 中断描述符表寄存器 IDTR

   与 GDTR 的作用类似,IDTR 寄存器用于存放中断描述符表 IDT 的 32 位线性基地址和 16 位表长度值。指令 LIDT 和 SIDT 分别用于加载和保存 IDTR 寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为 0,而长度值被设置成 0xFFFF。

3.局部描述符表寄存器 LDTR

 LDTR 寄存器中用于存放局部描述符表 LDT 的 32 位线性基地址、16 位段限长和描述符属性值。指令 LLDT 和 SLDT 分别用于加载和保存 LDTR 寄存器的段描述符部分。包含 LDT 表的段必须在 GDT 表中有一个段描述符项。当使用 LLDT 指令把含有 LDT 表段的选择符加载进 LDTR 时,LDT 段描述符的段基地址、段限长度以及描述符属性会被自动地加载到 LDTR 中。当进行任务切换时,处理器会把新任务 LDT的段选择符和段描述符自动地加载进 LDTR 中。在机器加电或处理器1位后,段选择符和基地址被默认地设置为 0,而段长度被设置成 0xFFFF。

    LDTR 寄存器中用于存放局部描述符表 LDT 的 32 位线性基地址、16 位段限长和描述符属性值。指令 LLDT 和 SLDT 分别用于加载和保存 LDTR 寄存器的段描述符部分。包含 LDT 表的段必须在 GDT 表中有一个段描述符项。当使用 LLDT 指令把含有 LDT 表段的选择符加载进 LDTR 时,LDT 段描述符的段基地址、段限长度以及描述符属性会被自动地加载到 LDTR 中。当进行任务切换时,处理器会把新任务 LDT的段选择符和段描述符自动地加载进 LDTR 中。在机器加电或处理器1位后,段选择符和基地址被默认地设置为 0,而段长度被设置成 0xFFFF。

4. 任务寄存器 TR

   TR 寄存器用于存放当前任务 TSS 段的 16 位段选择符、32 位基地址、16 位段长度和描述符属性值它引用 GDT 表中的一个 TSS 类型的描述符。指令 LTR 和 STR 分别用于加载和保存 TR 寄存器的段选择符部分。当使用 LTR 指令把选择符加载进任务寄存器时,TSS 描述符中的段基地址、段限长度以及描述符属性会被自动地加载到任务寄存器中。当执行任务切换时,处理器会把新任务 TSS 的段选择符和段描述符自动地加载进任务寄存器 TR 中。

3、控制寄存器

    控制寄存器(CR0、CR1、CR2 和 CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性, 见图 4-3 所示。CR0 中含有控制处理器操作模式和状态的系统控制标志;CR1 保留不用;CR2 含有导致页。错误的线性地址。CR3 中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器 PDBR(Page-Directory Base address Register)。

CR0 中协处理器控制位

   CR0 的 4 个比特位:扩展类型位 ET、任务切换位 TS、仿真位 EM 和数学存在位 MP 用于控制 80X86 浮点(数学)协处理器的操作。有关协处理器的详细说明请参见第 11 章内容。CR0 的 ET 位(标志)用于选择与协处理器进行通信所使用的协议,即指明系统中使用的是 80387 还是 80287 协处理器。TS、MP 和 EM 位用于确定浮点指令或 WAIT 指令是否应该产生一个设备不存在 DNA(Device Not Available)异常。这个异常可用来仅为使用浮点运算的任务保存和恢复浮点寄存器。对于没有使用浮点运算的任务,这样做可以加快它们之间的切换操作。(P86)


  启用保护模式 PE(Protected Enable) 位(位 0)和开启分页 PG(Paging) 位(位 31)分别用于控制分段和分页机制。PE 用于控制分段机制。如果 PE=1,处理器就工作在开启分段机制环境下,即运行在保护模式下。如果 PE=0,则处理器关闭了分段机制,并如同 8086 工作于实地址模式下。PG 用于控制分页机制。如果 PG=1,则开启了分页机制。如果 PG=0,分页机制被禁止,此时线性地址被直接作为物理地址使用。

  如果 PE=0、PG=0,处理器工作在实地址模式下;如果 PG=0、PE=1,处理器工作在没有开启分页机制的保护模式下;如果 PG=1、PE=0,此时由于不在保护模式下不能启用分页机制,因此处理器会产生一个一般保护异常,即这种标志组合无效;如果 PG=1、PE=1,则处理器工作在开启了分页机制的保护模式下。

  当改变 PE 和 PG 位时,我们必须小心。只有当执行程序起码有部分代码和数据在线性地址空间和物理地址空间中具有相同地址时,我们才能改变 PG 位的设置。此时这部分具有相同地址的代码在分页和未分页世界之间起着桥梁的作用。无论是否开启分页机制,这部分代码都具有相同的地址。另外,在开启分页(PG=1)之前必须先刷新 CPU 中的页高速缓冲(或称为转换查找缓冲区 TLB - Translation Lookaside Buffers)。

  在修改该了 PE 位之后程序必须立刻使用一条跳转指令,以刷新处理器执行管道中已经获取的不同模式下的任何指令。在设置 PE 位之前,程序必须初始化几个系统段和控制寄存器。在系统刚上电时,处理器被复位成 PE=0、PG=0(即实模式状态),以允许引导代码在启用分段和分页机制之前能够初始化这些寄存器和数据结构。


CR2 和 CR3

  CR2 和 CR3 用于分页机制。CR3 含有存放页目录表页面的物理地址,因此 CR3 也被称为 PDBR。因为页目录表页面是页对齐的,所以该寄存器只有高 20 位是有效的。而低 12 位保留供更高级处理器使用,因此在往 CR3 中加载一个新值时低 12 位必须设置为 0。

  使用 MOV 指令加载 CR3 时具有让页高速缓冲无效的副作用。为了减少地址转换所要求的总线周期数量,最近访问的页目录和页表会被存放在处理器的页高速缓冲器件中,该缓冲器件被称为转换查找缓冲区 TLB(Translation Lookaside Buffer)。只有当 TLB 中不包含要求的页表项时才会使用额外的总线周期从内存中读取页表项。

  即使 CR0 中的 PG 位处于复位状态(PG=0),我们也能先加载 CR3。以允许对分页机制进行初始化。当切换任务时,CR3 的内容也会随之改变。但是如果新任务的 CR3 值与原任务的一样,处理器就无需刷新页高速缓冲。这样共享页表的任务可以执行得更快。

  CR2 用于出现页异常时报告出错信息。在报告页异常时,处理器会把引起异常的线性地址存放在 CR2 中。因此操作系统中的页异常处理程序可以通过检查 CR2 的内容来确定线性地址空间中哪一个页面引发了异常。

Linux0.11 80X86知识(七)(中):https://developer.aliyun.com/article/1597274

目录
相关文章
|
Ubuntu 关系型数据库 MySQL
M1 macos docker获取x86 x64 amd 等指定架构版本linux ubuntu mysql 容器并启动容器
M1 macos docker获取x86 x64 amd 等指定架构版本linux ubuntu mysql 容器并启动容器
|
20天前
|
Ubuntu Linux
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
132 3
|
22天前
|
存储 Linux 调度
Linux0.11 80X86知识(七)(下)
Linux0.11 80X86知识(七)
44 0
|
22天前
|
存储 Linux 编译器
Linux0.11 80X86知识(七)(中)
Linux0.11 80X86知识(七)
20 0
|
4月前
|
消息中间件 测试技术 Linux
linux实时操作系统xenomai x86平台基准测试(benchmark)
本文是关于Xenomai实时操作系统的基准测试,旨在评估其在低端x86平台上的性能。测试模仿了VxWorks的方法,关注CPU结构、指令集等因素对系统服务耗时的影响。测试项目包括信号量、互斥量、消息队列、任务切换等,通过比较操作前后的时戳来测量耗时,并排除中断和上下文切换的干扰。测试结果显示了各项操作的最小、平均和最大耗时,为程序优化提供参考。注意,所有数据基于特定硬件环境,测试用例使用Alchemy API编写。
930 0
linux实时操作系统xenomai x86平台基准测试(benchmark)
|
4月前
|
存储 负载均衡 网络协议
X86 linux异常处理与Ipipe接管中断/异常
本文讲述了X86平台上Xenomai的ipipe如何接管中断处理。首先回顾了X86中断处理机制,包括IDT(中断描述符表)的工作原理和中断处理流程。接着详细介绍了Linux中中断门的初始化,包括门描述符的结构、中断门的定义和填充,以及IDT的加载。在异常处理部分,文章讲解了早期异常处理和start_kernel阶段的异常向量初始化。最后,讨论了APIC和SMP中断在IDT中的填充,以及剩余中断的统一处理。文章指出,ipipe通过在中断入口处插入`__ipipe_handle_irq()`函数,实现了对中断的拦截和优先处理,确保了实时性。
114 0
X86 linux异常处理与Ipipe接管中断/异常
|
10月前
|
网络协议 Linux 网络安全
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统 2
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统
219 0
|
10月前
|
存储 编解码 Linux
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统 1
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统
584 0
|
NoSQL 网络协议 安全
Linux系统:第十二章:AWS服务器X86架构安装配置Mysql与MongoDB
Linux系统:第十二章:AWS服务器X86架构安装配置Mysql与MongoDB
235 0
|
存储 Linux 芯片
Linux内核13_1-进程切换是对FPU单元的处理_X86
Linux内核13_1-进程切换是对FPU单元的处理_X86