0%

Linux内核完全解析

LINUX内核概况

从功能层面上 内核就是软件和硬件之间的一个中间层,可以连接软件和硬件。

内核实现策略

1.微内核。最基本的功能由中央内核(微内核)实现。所有其他的功能都委托给一些独立进程,这些进程通过明确定义的通信接口与中心内核通信。

2.宏内核。内核的所有代码,包括子系统(如内存管理、文件管理、设备驱动程序)都打包到一个文件中。内核中的每一个函数都可以访问到内核中所有其他部分。目前支持模块的动态装卸(裁剪)。Linux内核就是基于这个策略实现的。

img

X86寄存器

  1. 通用寄存器:

    • EAX(累加器寄存器):主要用于算术和逻辑运算,并作为函数返回值的容器。
    • EBX(基址寄存器):通常被用作内存地址的基址。
    • ECX(计数器寄存器):通常用于循环计数和字符串操作。
    • EDX(数据寄存器):用于存放一般性数据。
    • ESI(源变址寄存器):通常用于字符串操作中的源地址。
    • EDI(目的变址寄存器):通常用于字符串操作中的目的地址。
    • ESP(堆栈指针寄存器):指向当前堆栈顶部。
    • EBP(基址指针寄存器):通常用于指向栈帧的基址。
  2. 段寄存器:

    • CS(代码段寄存器):存储当前执行指令的代码段。
    • DS(数据段寄存器):存储数据的段。
    • SS(堆栈段寄存器):存储堆栈的段。
    • ES(附加段寄存器):可用于存储其他数据段。
  3. 标志寄存器:

    • EFLAGS(标志寄存器):用于存储各种程序状态标志,如进位标志、零标志、溢出标志等。

    • (PSW)

      在x86架构中,PSW是一个常见的缩写,它代表程序状态字(Program Status Word),也被称为标志寄存器或标志字。然而,在x86架构中,官方没有直接称之为PSW寄存器,而是使用了一系列标志位来表示程序的状态。

      这些标志位通常存储在EFLAGS寄存器中

  4. 指令指针寄存器:

    • EIP(指令指针寄存器):存储下一条要执行的指令的内存地址。

4fdf0e888ad57173cf481e5e1baea6f8.png

磁盘结构

image-20230628013256194

ROM 和RAM

ROM是一种只读存储器,其中存储的数据在计算机关闭或重新启动时保持不变。ROM中的数据是由制造商预先编程的,用户无法修改其内容。它通常用于存储固件、引导程序和其他永久性的系统数据。ROM的优点是数据的持久性和稳定性,即使在断电情况下也能保留数据。常见的ROM类型包括ROM、PROM(Programmable ROM)、EPROM(Erasable Programmable ROM)和EEPROM(Electrically Erasable Programmable ROM)。

RAM是一种随机访问存储器,用于临时存储计算机正在运行的数据和程序。RAM允许计算机以随机的顺序读取和写入数据,这使得存储器的访问速度非常快。RAM是易失性存储器,意味着当计算机断电时,存储在RAM中的数据将会丢失。RAM可以根据需要读取和写入数据,因此它对于计算机的实时操作非常重要。常见的RAM类型包括SRAM(Static RAM)和DRAM(Dynamic RAM)。

RAM和ROM在功能和特性上有很大的差异。RAM用于临时存储和处理数据,具有快速的读写速度,但数据不持久保存。ROM用于存储永久性数据和程序,不可修改,但数据的持久性较高。在计算机系统中,通常会同时使用RAM和ROM来满足不同的存储需求。

GNU AS和 AS86

GNU As(GNU汇编器)和 as86(GNU汇编器的一个特定版本)是两个不同的汇编器工具,它们具有以下区别:

  1. 功能和用途:
    • GNU As:GNU As是GNU项目中的一部分,是一个通用的汇编器,支持多种处理器架构(如x86、ARM等)和不同的操作系统。
    • as86:as86是GNU As的一个特定版本,专门用于处理x86架构的16位汇编语言。它主要用于早期的8086和80286处理器,并且适用于特定的操作系统,如DOS。
  2. 支持的架构:
    • GNU As:GNU As是一个通用的汇编器,支持多种处理器架构,包括x86、ARM、MIPS、PowerPC等。
    • as86:as86是为x86架构的16位汇编语言而设计的,主要用于早期的8086和80286处理器。
  3. 语法和特性:
    • GNU As:GNU As使用AT&T语法,具有丰富的指令集和功能,支持宏汇编、条件汇编、符号表等高级特性。
    • as86:as86通常使用Intel语法,语法更接近机器语言,功能相对较为简单,不支持高级特性。
  4. 可移植性:
    • GNU As:GNU As具有广泛的可移植性,可在不同的操作系统和处理器架构上使用。
    • as86:as86主要用于早期的x86架构和特定操作系统(如DOS),可移植性相对较低。

总的来说,GNU As是一个通用的汇编器,支持多种处理器架构和操作系统,而as86是GNU As的一个特定版本,专门用于处理早期的x86架构的16位汇编语言。

GNU AS语法

GNU As(GNU汇编器)使用AT&T语法,它与Intel语法在一些语法规则和指令书写上存在一些区别。

  1. 操作数顺序:AT&T语法使用源操作数在目的操作数之前的顺序,与Intel语法相反。 示例:
    • AT&T语法:movl %eax, %ebx(将eax的值移动到ebx)
    • Intel语法:mov ebx, eax(将eax的值移动到ebx)
  2. 寄存器表示:在AT&T语法中,寄存器名以%符号开头。 示例:
    • AT&T语法:movl $42, %eax(将立即数42移动到eax寄存器)
    • Intel语法:mov eax, 42(将立即数42移动到eax寄存器)
  3. 立即数表示:在AT&T语法中,立即数以$符号开头。 示例:
    • AT&T语法:movl $123, %ebx(将立即数123移动到ebx寄存器)
    • Intel语法:mov ebx, 123(将立即数123移动到ebx寄存器)
  4. 内存引用:在AT&T语法中,使用方括号[]表示内存引用,且地址放在括号内。 示例:
    • AT&T语法:movl (%eax), %ebx(将eax寄存器指向的内存位置的值移动到ebx寄存器)
    • Intel语法:mov ebx, [eax](将eax寄存器指向的内存位置的值移动到ebx寄存器)
  5. 注释:在AT&T语法中,使用分号;来表示注释。 示例:
    • AT&T语法:addl %eax, %ebx ; 这是一个加法指令(将eax的值加到ebx,并带有注释)

这些是AT&T语法在GNU As中的一些基本规则和示例。要编写符合GNU As语法的汇编代码,可以参考GNU As的相关文档或参考资料。

AS86语法

AS86是一个特定版本的GNU As汇编器,用于处理x86架构的16位汇编语言。AS86使用的是Intel语法,与AT&T语法有所不同

直接寄存器寻址

mov bx, axax的值直接复制到bx寄存器。

间接寄存器寻址

mov [bx], axax的值存储到bx寄存器内容指定的内存位置上。

把 立即数1234 放入ax中,把 立即数msgl 放入ax中

mov ax, #1234

mov ax, #msgl

绝对寻址:将内存地址1234,内存地址msgl处的内容放入ax中

mov ax, 1234

mov ax, msgl = mov ax, [msgl]

索引寻址

mov ax, msgl[bx] 将内存中以 msgl 加上 bx 寄存器的内容作为地址的位置上的值,读取到 ax 寄存器中。

  1. 计算内存地址:将 msgl 寄存器的内容与 bx 寄存器的内容相加,得到内存地址。
  2. 从内存中读取数据:从上一步计算得到的内存地址处读取数据。
  3. 将读取到的数据存储到 ax 寄存器:将读取到的数据移动到 ax 寄存器中。

mov [msgl+17], ah

将寄存器AH的值写入到内存中msgl的地址加上17的位置上。

C程序编译过程

image-20230626142646497

汇编程序如何调用执行C语言程序?

在x86汇编语言中,ESP(Extended Stack Pointer)和EBP(Extended Base Pointer)是两个寄存器,用于管理和访问程序运行时的堆栈(stack)。

  1. ESP(堆栈指针):ESP寄存器指向当前堆栈的栈顶,即最后一个压入堆栈的数据的位置。堆栈的增长方向是从高地址向低地址,所以ESP的值会随着数据的压入而减小,随着数据的弹出而增加。
  2. EBP(基指针):EBP寄存器用于建立一个指向当前函数堆栈帧(stack frame)的基准点。函数调用时,EBP通常会被设置为当前堆栈的值,然后用于在堆栈中访问函数参数和局部变量。通过使用EBP,可以在函数内部相对于堆栈帧的固定偏移量来访问变量,而不需要关心ESP的变化。
image-20230626152343475

image-20230626170727202

要在汇编程序中调用执行C语言程序,通常需要以下步骤:

  1. 编写C语言程序:首先,您需要编写一个C语言程序,实现所需的功能。保存该程序并确保已生成可执行文件。

  2. 准备汇编程序:接下来,您需要准备一个汇编程序,该程序将用于调用执行C语言程序。

  3. 设置堆栈:在汇编程序中,您需要设置堆栈指针(SP)和基指针(BP)以及其他必要的寄存器,以便在调用C语言程序时正确处理函数参数和返回值。

  4. 调用C语言程序:使用汇编指令,您可以调用C语言程序。您可以使用CALL指令将程序控制权传递给C语言程序的入口点,并使用RET指令从C语言程序返回到汇编程序。

  5. 处理函数参数和返回值:在调用C语言程序之前,您需要将函数参数加载到正确的寄存器或堆栈位置中。执行CALL指令后,C语言程序将在执行完毕后返回结果。您需要使用适当的方法来访问返回的结果。

    本书规定:

    1. 若返回值是一个整数或一个指针,那么寄存器 eax 将被默认用来传递返回值

    2. A调用B;

      A必须需要保存EAX,EDX,ECX的值

      B必须需要保存EBX, ESI, EDI的值

  6. 恢复堆栈:在从C语言程序返回到汇编程序后,您需要恢复堆栈指针(SP)和基指针(BP)以及其他寄存器的值。

x86汇编语言调用执行C语言程序

以下是一个使用x86汇编语言调用执行C语言程序的简单例子:

#include <stdio.h>
int add(int a, int b) { return a + b; }
int main() {
int result = add(5, 3);
printf("Result: %d\n", result);
return 0;
}

汇编程序(example.asm):

section .data 	; 这是数据段的开始,用于定义数据段中的变量和常量。在这个例子中,定义了一个格式化字符串format,用于在printf中输出结果。
format db "Result: %d", 10, 0

section .text ; 这是代码段的开始,用于定义可执行代码。在这个例子中,定义了一系列指令来调用执行C语言程序。
extern printf ; 这是外部声明,告诉汇编器在链接时要在其他地方找到printf函数的定义。
extern add ; 同样是外部声明,告诉汇编器在链接时要在其他地方找到add函数的定义。

global _start ; 这是指定程序入口点的全局声明,告诉链接器程序的入口点是_start标签。

_start: ; 程序的入口点,代码从这里开始执行。
; 这两条指令将堆栈指针(SP)和基指针(BP)设置为相同的值,以建立堆栈帧。
mov esp, ebp
mov ebp, esp

; 这两条指令将函数参数依次压入堆栈。
push dword 3
push dword 5

; 调用add函数,将程序控制权转移到该函数。
call add

; 将栈指针(Stack Pointer,SP)寄存器的值增加8个字节。
; 清理堆栈上的函数参数
add esp, 8

; 这条指令将add函数的返回值移动到通用寄存器EBX中,以便后续传递给printf函数。
mov ebx, eax

; 这两条指令将printf函数的参数依次压入堆栈。
push ebx
push format

; 调用printf函数,将结果输出到控制台。
call printf

; 清理堆栈上的printf函数的参数。
add esp, 8

; 这两条指令恢复堆栈指针和基指针的值,以便返回到程序的调用者。
mov esp, ebp
mov ebp, 0

mov eax, 1 ; 这条指令将系统调用号1(代表程序退出)移动到通用寄存器EAX中。
xor ebx, ebx ; 将通用寄存器EBX与自身进行异或操作,将其值设置为0,表示退出状态码为0。
int 0x80 ; 触发一个软中断,将控制权交给操作系统,以执行系统调用,这里是退出程序。

GNU AS语言调用执行C语言程序

image-20230626160359117

image-20230626161936812

image-20230626162008840

.text
_swap:
pushl %ebp ; 将 %ebp 寄存器的值压入堆栈。通常在函数开始时执行此操作,以保存调用者的基指针。
movl %esp, %ebp ; 将 %esp 寄存器(当前堆栈指针)的值复制到 %ebp 寄存器中。这为当前函数设置基指针。
subl $4, %esp ; 从 %esp 寄存器中减去 4,为局部变量或其他用途在堆栈上分配 4 字节的空间。
movl 8(%ebp), %eax ; 将位于EBP寄存器指向的内存地址上偏移为8字节的值读取到EAX寄存器中。这可能是从堆栈中检索参数或变量。
movl (%eax), %ecx ; 将 %eax 寄存器中存储的地址处的值加载到 %ecx 寄存器中。这检索 %eax 寄存器指向的值。
movl %ecx, -4(%ebp) ; 将 %ecx 寄存器中的值存储在内存位置 -4(%ebp)。这可能是将检索到的值存储在堆栈上。
movl 8(%ebp), %eax ; 再次将地址 8(%ebp) 处的值加载到 %eax 寄存器中。
movl 12(%ebp), %edx ; 将地址 12(%ebp) 处的值加载到 %edx 寄存器中。这可能是从堆栈中检索另一个参数或变量。
movl (%edx), %ecx ; 将 %edx 寄存器中存储的地址处的值加载到 %ecx 寄存器中。这检索 %edx 寄存器指向的值。
movl %ecx, (%eax) ; 将 %ecx 寄存器中的值存储在内存位置 %eax 处。
movl 12(%ebp), %eax ; 再次将内存地址 12(%ebp) 处的值加载到 %eax 寄存器中。
movl -4(%ebp), %ecx ; 将内存位置 -4(%ebp) 处的值加载到 %ecx 寄存器中。
movl %ecx, (%eax) ; 将 %ecx 寄存器中的值存储在内存位置 %eax 处。
leave ; 撤销当前函数的堆栈帧。
ret ; 从当前函数返回。
leave
=
movl %ebp, %esp
popl %ebp

LINUX源码分析

LINUX操作系统引导启动程序(boot/bootsect.s & setup.s & head.s)

  1. 电源打开后,80x86结构的CPU会进入实模式。【实模式是一种最初用于早期x86处理器的模式,它提供对低级硬件的基本访问能力】

  2. CPU会从固定地址0xFFFF0开始执行代码。该地址上存储着ROM-BIOS的跳转指令。

  3. BIOS执行系统的初始化和自检过程。它会进行一系列的硬件检测和配置,包括检测内存、处理器、硬盘、显卡等,并建立基本的中断向量表。

  4. BIOS会从可启动设备的第一个扇区(磁盘的引导扇区)读取512字节的代码,将其加载到内存的绝对地址0x7C00处。

  5. image-20230627185638445

  6. 然后将整个系统从地址 0x10000 移至0x0000 处,进入保护模式并跳转至系统的余下部分(在0x0000处)。此时所有32 位运行方式的设置启动被完成:IDT、GDT 以及LDT 被加载,处理器和协处理器也己确认,分页工作也设置好了;

  7. 最终调用 init/main.c 中的main()程序。上述操作的源代码是在 boot/head.s中的。注意如果在前述任何一步中出了错,计算机就会死锁。

另外,仅在内存中加载了上述内核代码模块并不能让Linux 系统运行起来。完整可运行的 Linux系统还需要有一个基本的根文件系统。Linux 0.11 内核仅支持 MINIX 的 1.0 文件系统。根文件系统通常是在另一个软盘上或者在一个硬盘分区中。为了通知内核所需要的根文件系统在什么地方,bootsect.s 程序的第 43 行上给出了根文件系统所在的默认块设备号。在内核初始化时会使用编译内校时放在引导扇区第 509、510 (0x1fc-0x1fd)字节中的指定设备号。

总结:

image-20230627184155816

image-20230628170626993

s