代码编织梦想

📖 操作系统实验—— Linux启动初始化过程设计探析实验

Linux启动初始化过程设计探析实验

一、 实验目的

探索、分析和理解操作系统引导及自启动初始化的基本流程、设计机理。

二、 实验内容

实验主要完成的内容如下:

  • 下载和研读Linux内核源码(本实验研读的是linux 0.11版本)
  • 探索、分析和理解操作系统引导和自启动初始化的基本流程
  • 完成相应Linux内核源码的编译和启用
  • 并在虚拟机平台上加以测试验证

Linux启动初始化过程探析实验基本要求如下:

(1)下载和研读Linux内核源码(可以是任意版本);

(2)围绕操作系统引导和自启动初始化过程,研读Linux内核对应源码(包括汇编代码、C程序、Makefile及相关配置文件),整理操作系统引导及自启动初始化的基本流程和设计机理(包括处理器平台相关部分和无关部分,前者可针对x86体系结构或MIPS体系结构或arm64体系结构); (3)完成相应Linux内核源码的编译和启用;

(4)在虚拟机平台上启用相应Linux内核并测试验证,对照启动时所显示的系统信息和自己关于内核源码分析结果的一致性。

三、 内核源码研读

3.1 引导启动程序Boot目录概述

linux内核源码文件结构图如下:

在这里插入图片描述

其中Boot目录下的文件负责引导和启动,目录中含有3个汇编语言文件:

  • bootsect.s 程序是磁盘引导块程序,编译后会驻留在磁盘的第一个扇区中(引导扇区,0磁道(柱面),0磁头,第1个扇区)。在PC机加电ROM BIOS自检后,将被BIOS加载到内存Ox7CO0处进行执行。
  • setup.s程序主要用于读取机器的硬件配置参数,并把内核模块system移动到适当的内存位置处。
  • head.s程序会被编译连接在system模块的最前部分,主要进行硬件设备的探测设置和内存管理页面

这三个文件是内核源代码文件中最先被编译的程序。这3个程序完成的主要功能是当计算机加电时引导内核启动,将内核代码加载到内存中,并做一些进入32位保护运行方式前的系统初始化工作。

其中 bootsect.s和 setup.s程序是运行在实模式下的16位代码程序,使用的是 as86的汇编语言格式,编译之后会生成二进制文件和目标文件(.o);head.s则是运行在保护模式下的32位的代码程序,需要用GNU as来编译,使用的是AT&T格式的汇编语言, 编译之后只会生成对应的目标文件。具体的文件目录如下:

在这里插入图片描述

3.2 BIOS的启动原理

加电的一瞬间,计算机的内存中(RAM)什么程序也没有。软盘里虽然有操作系统程序,但CPU的逻辑电路被设计为只能运行内存中的程序,没有能力直接从软盘运行操作系统。如果要运行软盘中的操作系统,必须将软盘中的操作系统程序加载到内存(RAM)中。

Intel 80x86系列的CPU可以分别在16位实模式和32位保护模式下运行。为了兼容,也为了解决最开始的启动问题,Intel将所有80x86系列的CPU,包括最新型号的CPU的硬件都设计为加电即进入16位实模式状态运行。将CPU硬件逻辑设计为加电瞬间强行将CS的值置为0xF000、IP的值置为0xFFF0,这样CS:IP就指向0xFFFF0这个地址位置。下图表明了启动时BIOS(示例中的BIOS程序8 KB,所占地址段为0xFE000~0xFFFFF)在内存的状态及初始执行位置:

在这里插入图片描述

然后BIOS程序在内存最开始的位置(0x00000)用1KB的内存空间(0x00000~0x003FF)构建中断向量表,在紧挨着它的位置用256字节的内存空间构建BIOS数据区(0x00400~0x004FF),并在大约57 KB以后的位置(0x0E05B)加载了8 KB左右的与中断向量表相应的若干中断服务程序。下图精确地标注了这些位置:

在这里插入图片描述

对于Linux0.11操作系统而言,计算机将分三批逐次加载操作系统的内核代码。第一批由BIOS中断int 0x19把第一扇区bootsect的内容加载到内存;第二批、第三批在bootsect的指挥下,分别把其后的4个扇区和随后的240个扇区的内容加载至内存。从3.3节开始,将分别介绍启动和加载的过程。

3.3 bootsect.s程序

3.3.1 bootsect原理概述

​ 软盘设置为启动设备,计算机硬件体系结构的设计与BIOS联手操作,会让CPU接收到一个int 0x19中断。CPU接收到这个中断后,会立即在中断向量表中找到int 0x19中断向量。中断向量把CPU指向0x0E6F2,这个位置就是int 0x19相对应的中断服务程序的入口地址,int 0x19中断向量所指向的中断服务程序,即启动加载服务程序,将软驱0号磁头对应盘面的0磁道1扇区的内容复制至内存0x07C00处。这个扇区里的内容就是Linux 0.11的引导程序(bootsect)其作用就是陆续把软盘中的操作系统程序载入内存。这样制作的第一扇区就称为启动扇区(boot sector)。下面的工作就是执行bootsect把软盘的第二批、第三批代码载入内存。

3.3.2 bootsect.s源码注释

bootsect.s程序注释如下:

!
! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
! versions of linux ! SYS_SIZE 是要加载的节数(16 字节为1 节)。
! 0x30000 字节=192 kB(上面Linus 估算错了),对于当前的版本空间已足够了。
!
SYSSIZE = 0x3000 ! 指编译连接后system 模块的大小。参见列表1.2 中第92 的说明。
! 这里给出了一个最大默认值。

! bootsect.s 被bios-启动子程序加载至0x7c00 (31k)处,并将自己
! 移到了地址0x90000 (576k)处,并跳转至那里。
!
! 它然后使用BIOS 中断将'setup'直接加载到自己的后面(0x90200)(576.5k),
! 并将system 加载到地址0x10000 处。
!
!为了把第二批和第三批程序加载到内存中的适当位置,bootsect首先做的工作就是规划内存。这些源代码的作用就是对后续操作所涉及的内存位置进行设置,包括将要加载的setup程序的扇区数(SETUPLEN)以及被加载到的位置(SETUPSEG);启动扇区被BIOS加载的位置(BOOTSEG)及将要移动到的新位置(INITSEG);内核(kernel)被加载的位置(SYSSEG)、内核的末尾位置(ENDSEG)及根文件系统设备号(ROOT_DEV)。

.globl begtext, begdata, begbss, endtext, enddata, endbss ! 定义了6 个全局标识符;
.text ! 文本段;
begtext:
.data ! 数据段;
begdata:
.bss ! 堆栈段;
begbss:
.text ! 文本段;

SETUPLEN = 4 ! nr of setup-sectors
! setup 程序的扇区数(setup-sectors)值;
BOOTSEG = 0x07c0 ! original address of boot-sector
! bootsect 的原始地址(是段地址,以下同);
INITSEG = 0x9000 ! we move boot here - out of the way
! 将bootsect 移到这里 -- 避开;
SETUPSEG = 0x9020 ! setup starts here
! setup 程序从这里开始;
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
! system 模块加载到0x10000(64 kB)处;
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
! 停止加载的段地址;

! ROOT_DEV: 0x000 - same type of floppy as boot.
! 根文件系统设备使用与引导时同样的软驱设备;
! 0x301 - first partition on first drive etc
! 根文件系统设备在第一个硬盘的第一个分区上,等等;
ROOT_DEV = 0x306 ! 指定根文件系统设备是第2 个硬盘的第1 个分区。这是Linux 老式的硬盘命名

!接下来,bootsect启动程序将它自身(全部的512B内容)从内存0x07C00(BOOTSEG)处复制至内存0x90000(INITSEG)处。
!在这次复制过程中,ds(0x07C0)和si(0x0000)联合使用,构成了源地址0x07C00;es(0x9000)和di(0x0000)联合使用,构成了目的地址0x90000,而mov cx, !256这一行循环控制量,提供了需要复制的“字”数(一个字为2字节,256个字正好是512字节,也就是第一扇区的字节数)
entry start ! 告知连接程序,程序从start 标号开始执行。
start: ! 47--56 行作用是将自身(bootsect)从目前段位置0x07c0(31k)
! 移动到0x9000(576k)处,共256 字(512 字节),然后跳转到
! 移动后代码的go 标号处,也即本程序的下一语句处。
mov ax,#BOOTSEG ! 将ds 段寄存器置为0x7C0;
mov ds,ax
mov ax,#INITSEG ! 将es 段寄存器置为0x9000;
mov es,ax
mov cx,#256 ! 移动计数值=256 字;
sub si,si ! 源地址 ds:si = 0x07C0:0x0000
sub di,di ! 目的地址 es:di = 0x9000:0x0000
!bootsect复制到新位置完毕后,会执行下面的代码
rep ! 重复执行,直到cx = 0
movw ! 移动1 个字;
!执行完这个跳转后,CS值变为0x9000(INITSEG),IP的值为从0x9000(INITSEG)到go: mov ax, cs这一行对应指令的偏移。换句话说,此时CS:IP指向go: mov ax, cs这一行,程序从这一行开始往下执行
jmpi go,INITSEG ! 间接跳转。这里INITSEG 指出跳转到的段地址。
!通过ax,用CS的值0x9000来把数据段寄存器(DS)、附加段寄存器(ES)、栈基址寄存器(SS)设置成与代码段寄存器(CS)相同的位置,并将栈顶指针SP指向偏移地址为0xFF00处
go: mov ax,cs ! 将ds、es 和ss 都置成移动后代码所在的段处(0x9000)。
mov ds,ax !由于程序中有堆栈操作(push,pop,call),因此必须设置堆栈。
mov es,ax
! put stack at 0x9ff00. ! 将堆栈指针sp 指向0x9ff00(即0x9000:0xff00)处
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
! 由于代码段移动过了,所以要重新设置堆栈段的位置。
! sp 只要指向远大于512 偏移(即地址0x90200)处
! 都可以。因为从0x90200 地址开始处还要放置setup 程序,
! 而此时setup 程序大约为4 个扇区,因此sp 要指向大
! 于(0x200 + 0x200 * 4 + 堆栈大小)处。

! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
! 在bootsect 程序块后紧根着加载setup 模块的代码数据。
! 注意es 已经设置好了。(在移动代码时es 已经指向目的段地址处0x9000)。

load_setup:
! 68--77 行的用途是利用BIOS 中断INT 0x13 将setup 模块从磁盘第2 个扇区
开始读到0x90200 开始处,共读4 个扇区。如果读出错,则复位驱动器,并重试,没有退路。INT 0x13 的使用方法如下:
! 读扇区:
! ah = 0x02 - 读磁盘扇区到内存;al = 需要读出的扇区数量;
! ch = 磁道(柱面)号的低8 位; cl = 开始扇区(0-5 位),磁道号高2 位(6-7);
! dh = 磁头号; dl = 驱动器号(如果是硬盘则要置位7);
! es:bx ??指向数据缓冲区; 如果出错则CF 标志置位。
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track
! 取磁盘驱动器的参数,特别是每道的扇区数量。
! 取磁盘驱动器参数INT 0x13 调用格式和返回信息如下:
! ah = 0x08 dl = 驱动器号(如果是硬盘则要置位7 为1)。
! 返回信息:
! 如果出错则CF 置位,并且ah = 状态码。
! ah = 0, al = 0, bl = 驱动器类型(AT/PS2)
! ch = 最大磁道号的低8 位,cl = 每磁道最大扇区数(位0-5),最大磁道号高2 位(位6-7)
! dh = 最大磁头数, dl = 驱动器数量,
! es:di -?? 软驱磁盘参数表。

mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13 !加载setup这个程序,要借助BIOS提供的int 0x13中断向量所指向的中断服务程序(也就是磁盘服务程序)来完成。将软盘第二扇区开始的4个扇区,即setup.s对应的程序加载至内存的SETUPSEG(0x90200)处。
mov ch,#0x00
seg cs ! 表示下一条语句的操作数在cs 段寄存器所指的段中。
mov sectors,cx ! 保存每磁道扇区数。
mov ax,#INITSEG
mov es,ax ! 因为上面取磁盘参数中断改掉了es 的值,这里重新改回。

! 接下来,bootsect程序要执行第三批程序的载入工作,即将系统模块载入内存。为了防止加载期间用户误认为是机器故障而执行不适当的操作,Linus在此设计了显示一行屏幕信息“Loading system...”以提示用户计算机此时正在加载系统
mov ah,#0x03 ! read cursor pos
xor bh,bh ! 读光标位置。
int 0x10

mov cx,#24 ! 共24 个字符。
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1 ! 指向要显示的字符串。
mov ax,#0x1301 ! write string, move cursor
int 0x10 ! 写字符串并移动光标。

!bootsect借着BIOS中断int 0x13,将240个扇区的system模块加载进内存。加载工作主要是由bootsect调用read_it子程序完成的。这个子程序将软盘第六扇区开始的约240个扇区的system模块加载至内存的SYSSEG(0x10000)处往后的120KB空间中。
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000 ! es = 存放system 的段地址。
call read_it ! 读磁盘上system 模块,es 为输入参数。
call kill_motor ! 关闭驱动器马达,这样就可以知道驱动器的状态了。

!确定一下根设备号
seg cs
mov ax,root_dev ! 将根设备号
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors ! 取上面第88 行保存的每磁道扇区数。如果sectors=15
! 则说明是1.2Mb 的驱动器;如果sectors=18,则说明是
! 1.44Mb 软驱。因为是可引导的驱动器,所以肯定是A 驱。
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15 ! 判断每磁道扇区数是否=15
je root_defined ! 如果等于,则ax 中就是引导驱动器的设备号。
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root: ! 如果都不一样,则死循环(死机)。
jmp undef_root
root_defined:
seg cs
mov root_dev,ax ! 将检查过的设备号保存起来。

!下面要通过执行“jmpi 0, SETUPSEG”这行语句跳转至0x90200处,就是前面讲过的第二批程序——setup程序加载的位置。CS:IP指向setup程序的第一条指令,意味着由setup程序接着bootsect程序继续执行
jmpi 0,SETUPSEG ! 跳转到0x9020:0000(setup.s 程序的开始处)。

! 下面是两个子程序。
! in: es - starting address segment (normally 0x1000)
! 该子程序将系统模块加载到内存地址0x10000 处,并确定没有跨越64KB 的内存边界。我们试图尽快
! 地进行加载,只要可能,就每次加载整条磁道的数据。
! 输入:es – 开始内存地址段值(通常是0x1000)
sread: .word 1+SETUPLEN ! sectors read of current track
! 当前磁道中已读的扇区数。开始时已经读进1 扇区的引导扇区
! bootsect 和setup 程序所占的扇区数SETUPLEN。
head: .word 0 ! current head !当前磁头号。
track: .word 0 ! current track !当前磁道号。

read_it:
! 测试输入的段值。必须位于内存地址64KB 边界处,否则进入死循环。清bx 寄存器,用于表示当前段内
! 存放数据的开始位置。
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary ! es 值必须位于64KB 地址边界!
xor bx,bx ! bx is starting address within segment ! bx 为段内偏移位置。
rp_read:
! 判断是否已经读入全部数据。比较当前所读段是否就是系统数据末端所处的段(#ENDSEG),如果不是就
! 跳转至下面ok1_read 标号处继续读数据。否则退出子程序返回。
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet? ! 是否已经加载了全部数据?
jb ok1_read
ret
ok1_read:
! 计算和验证当前磁道需要读取的扇区数,放在ax 寄存器中。
! 根据当前磁道还未读取的扇区数以及段内数据字节开始偏移位置,计算如果全部读取这些未读扇区,所
! 读总字节数是否会超过64KB 段长度的限制。若会超过,则根据此次最多能读入的字节数(64KB – 段内
! 偏移位置),反算出此次需要读取的扇区数。
seg cs
mov ax,sectors ! 取每磁道扇区数。
sub ax,sread ! 减去当前磁道已读扇区数。
mov cx,ax ! cx = ax = 当前磁道未读扇区数。
shl cx,#9 ! cx = cx * 512 字节。
add cx,bx ! cx = cx + 段内当前偏移值(bx)
! = 此次读操作后,段内共读入的字节数。
jnc ok2_read ! 若没有超过64KB 字节,则跳转至ok2_read 处执行。
je ok2_read
xor ax,ax ! 若加上此次将读磁道上所有未读扇区时会超过64KB,则计算
sub ax,bx ! 此时最多能读入的字节数(64KB – 段内读偏移位置),再转换
shr ax,#9 ! 成需要读取的扇区数。
ok2_read:
call read_track
mov cx,ax ! cx = 该次操作已读取的扇区数。
add ax,sread ! 当前磁道上已经读取的扇区数。
seg cs
cmp ax,sectors ! 如果当前磁道上的还有扇区未读,则跳转到ok3_read 处。
jne ok3_read
! 读该磁道的下一磁头面(1 号磁头)上的数据。如果已经完成,则去读下一磁道。
mov ax,#1
sub ax,head ! 判断当前磁头号。
jne ok4_read ! 如果是0 磁头,则再去读1 磁头面上的扇区数据。
inc track ! 否则去读下一磁道。
ok4_read:
mov head,ax ! 保存当前磁头号。
xor ax,ax ! 清当前磁道已读扇区数。
ok3_read:
mov sread,ax ! 保存当前磁道已读扇区数。
shl cx,#9 ! 上次已读扇区数*512 字节。
add bx,cx ! 调整当前段内数据开始位置。
jnc rp_read ! 若小于64KB 边界值,则跳转到rp_read(156 行)处,继续读数据。
! 否则调整当前段,为读下一段数据作准备。
mov ax,es
add ax,#0x1000 ! 将段基址调整为指向下一个64KB 段内存。
mov es,ax
xor bx,bx ! 清段内数据开始偏移值。
jmp rp_read ! 跳转至rp_read(156 行)处,继续读数据。

! 读当前磁道上指定开始扇区和需读扇区数的数据到es:bx 开始处。参见第67 行下对BIOS 磁盘读中断
! int 0x13,ah=2 的说明。
! al – 需读扇区数;es:bx – 缓冲区开始位置。
read_track:
push ax
push bx
push cx
push dx
mov dx,track ! 取当前磁道号。
mov cx,sread ! 取当前磁道上已读扇区数。
inc cx ! cl = 开始读扇区。
mov ch,dl ! ch = 当前磁道号。
mov dx,head ! 取当前磁头号。
mov dh,dl ! dh = 磁头号。
mov dl,#0 ! dl = 驱动器号(为0 表示当前驱动器)。
and dx,#0x0100 ! 磁头号不大于1。
mov ah,#2 ! ah = 2,读磁盘扇区功能号。
int 0x13
jc bad_rt ! 若出错,则跳转至bad_rt。
pop dx
pop cx
pop bx
pop ax
ret
! 执行驱动器复位操作(磁盘中断功能号0),再跳转到read_track 处重试。
bad_rt: mov ax,#0
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track

! 这个子程序用于关闭软驱的马达,这样我们进入内核后它处于已知状态,以后也就无须担心它了。
kill_motor:
push dx
mov dx,#0x3f2 ! 软驱控制卡的驱动端口,只写。
mov al,#0 ! A 驱动器,关闭FDC,禁止DMA 和中断请求,关闭马达。
outb ! 将al 中的内容输出到dx 指定的端口去。
pop dx
ret

sectors:
.word 0 ! 存放当前启动软盘每磁道的扇区数。

msg1:
.byte 13,10 ! 回车、换行的ASCII 码。
.ascii "Loading system ..."
.byte 13,10,13,10 ! 共24 个ASCII 码字符。

.org 508 ! 表示下面语句从地址508(0x1FC)开始,所以root_dev
! 在启动扇区的第508 开始的2 个字节中。
root_dev:
.word ROOT_DEV ! 这里存放根文件系统所在的设备号(init/main.c 中会用)。
boot_flag:
.word 0xAA55 ! 硬盘有效标识。

.text
endtext:
.data
enddata:
.bss
endbss:

3.4 setup.s程序

3.4.1 setup.s原理概述

接下来,操作系统要使计算机在32位保护模式下工作。首先关中断并将system移动到内存地址起始位置0x00000。将位于0x10000的内核程序复制至内存地址起始位置0x00000处。0x00000这个位置原来存放着由BIOS建立的中断向量表及BIOS数据区。这个复制动作将BIOS中断向量表和BIOS数据区完全覆盖,使它们不复存在。直到新的中断服务体系构建完毕之前,操作系统不再具备响应并处理中断的能力。setup程序继续为保护模式做准备。此时要通过setup程序自身提供的数据信息对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置。打开A20,实现32位寻址。进入保护模式后,将使用32位寻址模式,即采用32根地址线进行寻址,第21根(A20)至第32根地址线的选通控制将意味着寻址模式的切换。为了建立保护模式下的中断机制,setup程序将对可编程中断控制器8259A进行重新编程。

3.4.2 setup.s源码注释

setup.s 程序注释如下:

INITSEG = 0x9000 ! we move boot here - out of the way ! 原来bootsect 所处的段。
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536). ! system 在0x10000(64k)处。
SETUPSEG = 0x9020 ! this is the current segment ! 本程序所在的段地址。

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

entry start
start:

!setup程序现在开始执行。它做的第一件事情就是利用BIOS提供的中断服务程序从设备上提取内核运行所需的机器系统数据,其中包括光标位置、显示页面等数据,并分别从中断向量0x41和0x46向量值所指的内存地址处获取硬盘参数表1、硬盘参数表2,把它们存放在0x9000:0x0080和0x9000:0x0090处。
mov ax,#INITSEG 
mov ds,ax
mov ah,#0x03 ! read cursor pos
! BIOS 中断0x10 的读光标功能号 ah = 0x03
! 输入:bh = 页号
! 返回:ch = 扫描开始线,cl = 扫描结束线,
! dh = 行号(0x00 是顶端),dl = 列号(0x00 是左边)。
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
! 上两句是说将光标位置信息存放在0x90000 处,控制台初始化时会来取。

! Get memory size (extended mem, kB) ! 下面3 句取扩展内存的大小值(KB)。是调用中断0x15,功能号ah = 0x88 返回:ax = 从0x100000(1M)处开始的扩展内存大小(KB)。若出错则CF 置位,ax = 出错码。

mov ah,#0x88
int 0x15
mov [2],ax ! 将扩展内存数值存在0x90002 处(1 个字)。

! Get video-card data: ! 下面这段用于取显示卡当前显示模式。
! 调用BIOS 中断0x10,功能号 ah = 0x0f
! 返回:ah = 字符列数,al = 显示模式,bh = 当前显示页。
! 0x90004(1 字)存放当前页,0x90006 显示模式,0x90007 字符列数。

mov ah,#0x0f
int 0x10
mov [4],bx ! bh = display page
mov [6],ax ! al = video mode, ah = window width

! check for EGA/VGA and some config parameters ! 检查显示方式(EGA/VGA)并取参数。
! 调用BIOS 中断0x10,附加功能选择 -取方式信息
! 功能号:ah = 0x12,bl = 0x10
! 返回:bh = 显示状态
! (0x00 - 彩色模式,I/O 端口=0x3dX)
! (0x01 - 单色模式,I/O 端口=0x3bX)
! bl = 安装的显示内存
! (0x00 - 64k, 0x01 - 128k, 0x02 - 192k, 0x03 = 256k)
! cx = 显示卡特性参数(参见程序后的说明)。

mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax ! 0x90008 = ??
mov [10],bx ! 0x9000A = 安装的显示内存,0x9000B = 显示状态(彩色/单色)
mov [12],cx ! 0x9000C = 显示卡特性参数。

! Get hd0 data ! 取第一个硬盘的信息(复制硬盘参数表)。
! 第1 个硬盘参数表的首地址竟然是中断向量0x41 的向量值!而第2 个硬盘
! 参数表紧接第1 个表的后面,中断向量0x46 的向量值也指向这第2 个硬盘
! 的参数表首址。表的长度是16 个字节(0x10)。
! 下面两段程序分别复制BIOS 有关两个硬盘的参数表,0x90080 处存放第1 个
! 硬盘的表,0x90090 处存放第2 个硬盘的表。

mov ax,#0x0000
mov ds,ax
lds si,[4*0x41] ! 取中断向量0x41 的值,也即hd0 参数表的地址??ds:si
mov ax,#INITSEG
mov es,ax
mov di,#0x0080 ! 传输的目的地址: 0x9000:0x0080 ?? es:di
mov cx,#0x10 ! 共传输0x10 字节。
rep
movsb

! Get hd1 data

mov ax,#0x0000
mov ds,ax
lds si,[4*0x46] ! 取中断向量0x46 的值,也即hd1 参数表的地址??ds:si
mov ax,#INITSEG
mov es,ax
mov di,#0x0090 ! 传输的目的地址: 0x9000:0x0090 ?? es:di
mov cx,#0x10
rep
movsb

! Check that there IS a hd1 :-) ! 检查系统是否存在第2 个硬盘,如果不存在则第2 个表清零。
! 利用BIOS 中断调用0x13 的取盘类型功能。
! 功能号 ah = 0x15;
! 输入:dl = 驱动器号(0x8X 是硬盘:0x80 指第1 个硬盘,0x81 第2 个硬盘)
! 输出:ah = 类型码;00 --没有这个盘,CF 置位; 01 --是软驱,没有change-line 支持;
! 02 --是软驱(或其它可移动设备),有change-line 支持; 03 --是硬盘。

mov ax,#0x01500
mov dl,#0x81
int 0x13
jc no_disk1
cmp ah,#3 ! 是硬盘吗?(类型 = 3 ?)。
je is_disk1
no_disk1:
mov ax,#INITSEG ! 第2 个硬盘不存在,则对第2 个硬盘表清零。
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
is_disk1:

! 关中断并将system移动到内存地址起始位置0x00000
cli  ! 此时不允许中断。

!将位于0x10000的内核程序复制至内存地址起始位置0x00000处
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax 
add ax,#0x1000
cmp ax,#0x9000 ! 已经把从0x8000 段开始的64k
jz end_move
mov ds,ax ! source segment ! ds:si??源地址(初始为0x1000:0x0)
sub di,di
sub si,si
mov cx,#0x8000 ! 移动0x8000 字(64k 字节)。
rep
movsw
jmp do_move
!setup程序继续为保护模式做准备。此时要通过setup程序自身提供的数据信息对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置。
end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax ! ds 指向本程序(setup)段。
!此处的数据区域是在内核源代码中设定、编译并直接加载至内存形成的一块数据区域。专用寄存器的指向由程序中的lidt和lgdt指令完成
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate

!下面是标志性的动作——打开A20。打开A20,意味着CPU可以进行32位寻址,最大寻址空间为4 GB
call empty_8042 ! 等待输入缓冲器空。
! 只有当输入缓冲器为空时才可以对其进行写命令。
mov al,#0xD1 ! command write ! 0xD1 命令码-表示要写数据到
out #0x64,al ! 8042 的P2 端口。P2 端口的位1 用于A20 线的选通。
! 数据要写到0x60 口。
call empty_8042 ! 等待输入缓冲器空,看命令是否被接受。
mov al,#0xDF ! A20 on ! 选通A20 地址线的参数。
out #0x60,al
call empty_8042 ! 输入缓冲器为空,则表示A20 线已经选通。
!为了建立保护模式下的中断机制,setup程序将对可编程中断控制器8259A进行重新编程。
mov al,#0x11 ! initialization sequence
! 0x11 表示初始化命令开始,是ICW1 命令字,表示边
! 沿触发、多片8259 级连、最后要发送ICW4 命令字。
out #0x20,al ! send it to 8259A-1 ! 发送到8259A 主芯片。
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2 ! $ 表示当前指令的地址,
! 两条跳转指令,跳到下一条指令,起延时作用。
out #0xA0,al ! and to 8259A-2 ! 再发送到8259A 从芯片。
.word 0x00eb,0x00eb
mov al,#0x20 ! start of hardware int's (0x20)
out #0x21,al ! 送主芯片ICW2 命令字,起始中断号,要送奇地址。
.word 0x00eb,0x00eb
mov al,#0x28 ! start of hardware int's 2 (0x28)
out #0xA1,al ! 送从芯片ICW2 命令字,从芯片的起始中断号。
.word 0x00eb,0x00eb
mov al,#0x04 ! 8259-1 is master
out #0x21,al ! 送主芯片ICW3 命令字,主芯片的IR2 连从芯片INT。
.word 0x00eb,0x00eb !参见代码列表后的说明。
mov al,#0x02 ! 8259-2 is slave
out #0xA1,al ! 送从芯片ICW3 命令字,表示从芯片的INT 连到主芯
! 片的IR2 引脚上。
.word 0x00eb,0x00eb
mov al,#0x01 ! 8086 mode for both
out #0x21,al ! 送主芯片ICW4 命令字。8086 模式;普通EOI 方式,
! 需发送指令来复位。初始化结束,芯片就绪。
.word 0x00eb,0x00eb
out #0xA1,al !送从芯片ICW4 命令字,内容同上。
.word 0x00eb,0x00eb
mov al,#0xFF ! mask off all interrupts for now
out #0x21,al ! 屏蔽主芯片所有中断请求。
.word 0x00eb,0x00eb
out #0xA1,al !屏蔽从芯片所有中断请求。


! setup程序通过下面代码的前两行将CPU工作方式设为保护模式。将CR0寄存器第0位(PE)置1,即设定处理器工作方式为保护模式。
mov ax,#0x0001 ! protected mode (PE) bit ! 保护模式比特位(PE)。
lmsw ax !  就这样加载机器状态字!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs) ! 跳转至cs 段8,偏移0 处。
! 我们已经将system 模块移动到0x00000 开始的地方,所以这里的偏移地址是0。这里的段
! 值的8 已经是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。
! 段选择符长度为16 位(2 字节);位0-1 表示请求的特权级0-3,linux 操作系统只
! 用到两级:0 级(系统级)和3 级(用户级);位2 用于选择全局描述符表(0)还是局部描
! 述符表(1);位3-15 是描述符表项的索引,指出选择第几项描述符。所以段选择符
! 8(0b0000,0000,0000,1000)表示请求特权级0、使用全局描述符表中的第1 项,该项指出
! 代码的基地址是0(参见209 行),因此这里的跳转指令就会去执行system 中的代码。

! 下面这个子程序检查键盘命令队列是否为空。这里不使用超时方法 - 如果这里死机,
! 则说明PC 机有问题,我们就没有办法再处理下去了。
! 只有当输入缓冲器为空时(状态寄存器位2 = 0)才可以对其进行写命令。
empty_8042:
.word 0x00eb,0x00eb ! 这是两个跳转指令的机器码(跳转到下一句),相当于延时空操作。
in al,#0x64 ! 8042 status port ! 读AT 键盘控制器状态寄存器。
test al,#2 ! is input buffer full? ! 测试位2,输入缓冲器满?
jnz empty_8042 ! yes - loop
ret

gdt: ! 全局描述符表开始处。描述符表由多个8 字节长的描述符项组成。
! 这里给出了3 个描述符项。第1 项无用(206 行),但须存在。第2 项是系统代码段
! 描述符(208-211 行),第3 项是系统数据段描述符(213-216 行)。每个描述符的具体
! 含义参见列表后说明。
.word 0,0,0,0 ! dummy ! 第1 个描述符,不用。
! 这里在gdt 表中的偏移量为0x08,当加载代码段寄存器(段选择符)时,使用的是这个偏移值。
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
! 这里在gdt 表中的偏移量是0x10,当加载数据段寄存器(如ds 等)时,使用的是这个偏移值。
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386

idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L

gdt_48:
.word 0x800 ! gdt limit=2048, 256 GDT entries
! 全局表长度为2k 字节,因为每8 字节组成一个段描述符项
! 所以表中共可有256 项。
.word 512+gdt,0x9 ! gdt base = 0X9xxxx
! 4 个字节构成的内存线性地址:0x0009<<16 + 0x0200+gdt
! 也即0x90200 + gdt(即在本程序段中的偏移地址,205 行)。

.text
endtext:
.data
enddata:
.bss
endbss:

3.5 head.s程序

3.5.1 head.s原理概述

head程序与它们的加载方式与bootset和setup有所不同。bootsect和setup 是分别加载和执行的,但head是先将head.s汇编成目标代码,将用C语言编写的内核程序编译成目标代码,然后链接成system模块。head程序除了做一些调用main的准备工作之外,还用程序自身的代码在程序自身所在的内存空间创建了内核分页机制,即在0x000000的位置创建了页目录表、页表、缓冲区、GDT、IDT,并将head程序已经执行过的代码所占内存空间覆盖。这意味着head程序自己将自己废弃,main函数即将开始执行。

3.5.2 head.s源码解读

/*
* head.s 含有32 位启动代码。
* 注意!!! 32 位启动代码是从绝对地址0x00000000 开始的,这里也同样是页目录将存在的地方,
* 因此这里的启动代码将被页目录覆盖掉。
*/
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir: # 标号_pg_dir标识内核分页机制完成后的内核起始位置,也就是物理内存的起始位置0x000000。head程序马上就要在此处建立页目录表,为分页机制做准备
startup_32: # 18-22 行设置各个数据段寄存器。
#从这里开始,要将DS、ES、FS和GS等其他寄存器从实模式转变到保护模式。执行完毕后,DS、ES、FS和GS中的值都成为0x10。
movl $0x10,%eax # 对于GNU 汇编来说,每个直接数要以'$'开始,否则是表示地址。
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp # SS现在也要转变为栈段选择符,栈顶指针也成为32位的esp
call setup_idt # 调用设置中断描述符表子程序。
call setup_gdt # 调用设置全局描述符表子程序。
#调整DS、ES等寄存器
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs # 因为修改了gdt,所以需要重新装载所有的段寄存器。
mov %ax,%gs # CS 代码段寄存器已经在setup_gdt 中重新加载过了。
#现在,栈顶指针esp指向user_stack数据结构的外边缘,也就是内核栈的栈底。这样,当后面的程序需要压栈时,就可以最大限度地使用栈空间。
lss _stack_start,%esp
# 因为A20地址线是否打开影响保护模式是否有效,所以,要检验A20地址线是否确实打开了
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b # '1b'表示向后(backward)跳转到标号1 去(33 行)。
# 若是'5f'则表示向前(forward)跳转到标号5 去。

# 下面这段程序(43-65)用于检查数学协处理器芯片是否存在。
movl %cr0,%eax # check math chip
andl $0x80000011,%eax # Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
orl $2,%eax # set MP
movl %eax,%cr0
call check_x87
jmp after_page_tables # 跳转到135 行。

/*
* 我们依赖于ET 标志的正确性来检测287/387 存在与否。
*/
check_x87:
fninit
fstsw %ax
cmpb $0,%al
je 1f /* no coprocessor: have to set bits */
movl %cr0,%eax # 如果存在的则向前跳转到标号1 处,否则改写cr0。
xorl $6,%eax /* reset MP, set EM */
movl %eax,%cr0
ret
.align 2 # 这里".align 2"的含义是指存储边界对齐调整。"2"表示调整到地址最后2 位为零,
# 即按4 字节方式对齐内存地址。
1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ # 287 协处理器码。
ret

/*
* 下面这段是设置中断描述符表子程序 setup_idt
*
* 将中断描述符表idt 设置成具有256 个项,并都指向ignore_int 中断门。然后加载中断
* 描述符表寄存器(用lidt 指令)。真正实用的中断门以后再安装。当我们在其它地方认为一切
* 都正常时再开启中断。该子程序将会被页表覆盖掉。
*/
# 中断描述符表中的项虽然也是8 字节组成,但其格式与全局表中的不同,被称为门描述符
# (Gate Descriptor)。它的0-1,6-7 字节是偏移量,2-3 字节是选择符,4-5 字节是一些标志。
setup_idt:
lea ignore_int,%edx # 将ignore_int 的有效地址(偏移值)值??edx 寄存器
movl $0x00080000,%eax # 将选择符0x0008 置入eax 的高16 位中。
movw %dx,%ax /* selector = 0x0008 = cs */
# 偏移值的低16 位置入eax 的低16 位中。此时eax 含有
#门描述符低4 字节的值。
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
# 此时edx 含有门描述符高4 字节的值。
lea _idt,%edi # _idt 是中断描述符表的地址。
mov $256,%ecx
rp_sidt:
movl %eax,(%edi) # 将哑中断门描述符存入表中。
movl %edx,4(%edi)
addl $8,%edi # edi 指向表中下一项。
dec %ecx
jne rp_sidt
lidt idt_descr # 加载中断描述符表寄存器值。
ret

/*
* 设置全局描述符表项 setup_gdt
* 这个子程序设置一个新的全局描述符表gdt,并加载。此时仅创建了两个表项,与前
* 面的一样。该子程序只有两行,“非常的”复杂,所以当然需要这么长的注释了?。
setup_gdt:
lgdt gdt_descr # 加载全局描述符表寄存器(内容已设置好,见232-238 行)。
ret
/* Linus 将内核的内存页表直接放在页目录之后,使用了4 个表来寻址16 Mb 的物理内存。
* 如果你有多于16 Mb 的内存,就需要在这里进行扩充修改。
*/
# 每个页表长为4 Kb 字节,而每个页表项需要4 个字节,因此一个页表共可以存放1000 个表项,
# 如果一个表项寻址4 Kb 的地址空间,则一个页表就可以寻址4 Mb 的物理内存。
# 页表项的格式为:项的前0-11 位存放一些标志,如是否在内存中(P 位0)、读写许可(R/W 位1)、
# 普通用户还是超级用户使用(U/S 位2)、是否修改过(是否脏了)(D 位6)等;表项的位12-31 是
# 页框地址,用于指出一页内存的物理起始地址。
.org 0x1000 # 从偏移0x1000 处开始是第1 个页表(偏移0 开始处将存放页表目录)。
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000 # 定义下面的内存数据块从偏移0x5000 处开始。

/* 当DMA(直接存储器访问)不能访问缓冲块时,下面的tmp_floppy_area 内存块
* 就可供软盘驱动程序使用。其地址需要对齐调整,这样就不会跨越64kB 边界。
*/
_tmp_floppy_area:
.fill 1024,1,0 # 共保留1024 项,每项1 字节,填充数值0。

# 下面这几个入栈操作(pushl)用于为调用/init/main.c 程序和返回作准备。
# 前面3 个入栈指令不知道作什么用的,也许是Linus 用于在调试时能看清机器码用的?。
# 139 行的入栈操作是模拟调用main.c 程序时首先将返回地址入栈的操作,所以如果
# main.c 程序真的退出时,就会返回到这里的标号L6 处继续执行下去,也即死循环。
# 140 行将main.c 的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后
# 执行'ret'返回指令时就会将main.c 程序的地址弹出堆栈,并去执行main.c 程序去了。
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0 # 这些是调用main 程序的参数(指init/main.c)。
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main # '_main'是编译程序对main 的内部表示方法。
jmp setup_paging # 跳转至第198 行。
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.

/* This is the default interrupt "handler" :-) */
/* 下面是默认的中断“向量句柄”? */
int_msg:
.asciz "Unknown interrupt\n\r" # 定义字符串“未知中断(回车换行)”。
.align 2 # 按4 字节方式对齐内存地址。
ignore_int:
pushl %eax
pushl %ecx
pushl %edx
push %ds # 这里请注意!!ds,es,fs,gs 等虽然是16 位的寄存器,但入栈后
# 仍然会以32 位的形式入栈,也即需要占用4 个字节的堆栈空间。
push %es
push %fs
movl $0x10,%eax # 置段选择符(使ds,es,fs 指向gdt 表中的数据段)。
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
pushl $int_msg # 把调用printk 函数的参数指针(地址)入栈。
call _printk # 该函数在/kernel/printk.c 中。
# '_printk'是printk 编译后模块中的内部表示法。
popl %eax
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret # 中断返回(把中断调用时压入栈的CPU 标志寄存器(32 位)值也弹出)。


.align 2 # 按4 字节方式对齐内存地址边界。
setup_paging: # 首先对5 页内存(1 页目录 + 4 页页表)清零
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
# 页目录从0x000 地址开始。
cld;rep;stosl
# 下面4 句设置页目录中的项,我们共有4 个页表所以只需设置4 项。
# 页目录项的结构与页表中项的结构一样,4 个字节为1 项。参见上面113 行下的说明。
# "$pg0+7"表示:0x00001007,是页目录表中的第1 项。
# 则第1 个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000;
# 第1 个页表的属性标志 = 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写。
movl $pg0+7,_pg_dir /* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
# 下面6 行填写4 个页表中所有项的内容,共有:4(页表)*1024(项/页表)=4096 项(0 - 0xfff),
# 也即能映射物理内存 4096*4Kb = 16Mb。
# 每项的内容是:当前项所映射的物理内存地址 + 该页的标志(这里均为7)。
# 使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项在页表中的
# 位置是1023*4 = 4092。因此最后一页的最后一项的位置就是$pg3+4092。
movl $pg3+4092,%edi # edi??最后一页的最后一项。
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
# 最后1 项对应物理内存页面的地址是0xfff000,
# 加上属性标志7,即为0xfff007.
std # 方向位置位,edi 值递减(4 字节)。
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax # 每填写好一项,物理地址值减0x1000。
jge 1b # 如果小于0 则说明全添写好了。
# 设置页目录基址寄存器cr3 的值,指向页目录表。
xorl %eax,%eax /* pg_dir is at 0x0000 */ # 页目录表在0x0000 处。
movl %eax,%cr3 /* cr3 - page directory start */
# 设置启动使用分页处理(cr0 的PG 标志,位31)
movl %cr0,%eax
orl $0x80000000,%eax # 添上PG 标志。
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
# 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
# 该返回指令的另一个作用是将堆栈中的main 程序的地址弹出,并开始运行/init/main.c 程序。
# 本程序到此真正结束了。

.align 2 # 按4 字节方式对齐内存地址边界。
.word 0
idt_descr: #下面两行是lidt 指令的6 字节操作数:长度,基址。
.word 256*8-1 # idt contains 256 entries
.long _idt
.align 2
.word 0
gdt_descr: # 下面两行是lgdt 指令的6 字节操作数:长度,基址。
.word 256*8-1 # so does gdt (not that that's any
.long _gdt # magic number, but it works for me :^)

.align 3 # 按8 字节方式对齐内存地址边界。
_idt: .fill 256,8,0 # idt is uninitialized # 256 项,每项8 字节,填0。

# 全局表。前4 项分别是空项(不用)、代码段描述符、数据段描述符、系统段描述符,其中
# 系统段描述符linux 没有派用处。后面还预留了252 项的空间,用于放置所创建任务的
# 局部描述符(LDT)和对应的任务状态段TSS 的描述符。
# (0-nul, 1-cs, 2-ds, 3-sys, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...)
_gdt: .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a0000000fff /* 16Mb */ # 代码段最大长度16M。
.quad 0x00c0920000000fff /* 16Mb */ # 数据段最大长度16M。
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */

3.6 启动过程总结

linux0.11引导和启动过程主要分为两部:第一部分为加载操作系统;第二部分为32位保护、分页模式下的main函数的执行做准备。首先通过BIOS将bootsect.s文件加载到内存,然后执行bootset程序加载setup.s文件和system文件,从而完成操作系统程序的加载。接下来设置IDT、GDT、页目录表、页表以及机器系统数据,为32位保护、分页模式下的main函数的执行做准备。最后,跳转到main函数执行入口,开始执行main函数。

四、实验过程

4.1 实验环境部署

4.1.1 虚拟机的创建

首先在Vmware Workstation 搭建虚拟机Ubuntu16.04(32位)

ubuntu16.04官方下载地址:https://releases.ubuntu.com/16.04/ ,然后选择如下的ISO文件下载。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RaylBb9-1652110969607)(C:\Users\zpy\AppData\Roaming\Typora\typora-user-images\image-20220305160753271.png)]

4.1.2 上传实验环境和linux 0.11源码

编写安装实验环境的脚本setup.sh:

#!/bin/sh
# Beijing JiaoTong University
# Operating System - Setup Script v0.0.1
# $Author$: Zhang Yuwei<19291255@bjtu.edu.cn>
# $Date$: 2022-3-5

#配置OSLAB的地址为/home/ZYWoslab
export OSLAB_INSTALL_PATH=$HOME/ZYWoslab
#显示icon以及提示信息
cat mylabicon.icon
echo "|                 Environment Setup Script :)                   |"
echo "|                $ \033[34mZhang Yuwei\033[0m <19291255@bjtu.edu.cn> $                  |"
echo "+-------------------------------------------------------------------+"
# 为x86体系架构安装gcc编译器,使其可以编译32位的代码文件
install_gcc34_i386() {
    echo -n "* Install gcc-3.4 for x86(i386) arch now......"
    if [ -z `which gcc-3.4` ]; then
        sudo dpkg -i gcc-3.4/i386/gcc-3.4-base_3.4.6-8ubuntu2_i386.deb > /dev/null
        sudo dpkg -i gcc-3.4/i386/cpp-3.4_3.4.6-8ubuntu2_i386.deb > /dev/null
        sudo dpkg -i gcc-3.4/i386/gcc-3.4_3.4.6-8ubuntu2_i386.deb > /dev/null
        echo "\033[34mDone\033[0m"
    else
        echo "\033[33mSkipped\033[0m"
    fi
}

#编译Linux0.11 所需要的依赖
install_dep_i386() {
    echo "* Install dependencies for x86(i386) arch now......"
    sudo apt-get install bin86
    sudo apt-get install build-essential
    echo "* Install dependencies for x86(i386) arch now......\033[34mDone\033[0m"
}

configure_for_i386() {
    echo -n "* Copy rest files to ZYWoslab......"
    cp -r i386/* $OSLAB_INSTALL_PATH
    echo "\033[34mDone\033[0m"
}

# Common Code
if [ "$1" ] && ([ "$1" = "-s" ] || [ "$1" = "--skip-update" ]); then
    echo -n "* Begin to setup......\033[33m3\033[0m sec to start"; sleep 1
    echo -n "\r* Begin to setup......\033[33m2\033[0m sec to start"; sleep 1
    echo -n "\r* Begin to setup......\033[33m1\033[0m sec to start"; sleep 1
    echo "\r* Begin to setup......                                 \033[0m"
else
    echo -n "* Update apt sources......\033[33m3\033[31m sec to start"; sleep 1
    echo -n "\r* Update apt sources......\033[33m2\033[31m sec to start"; sleep 1
    echo -n "\r* Update apt sources......\033[33m1\033[31m sec to start"; sleep 1
    echo "\r* Update apt sources......                                 "
    sudo apt-get update
fi

# 创建OSLAB的下载路径,即/home/ZYWoslab
echo -n "* Create oslab main directory......"
[ -d $OSLAB_INSTALL_PATH ] || mkdir $OSLAB_INSTALL_PATH
echo "\033[34mDone\033[0m"

#创建linux-0.11的下载路径
echo -n "* Create linux-0.11 directory......"
[ -d $OSLAB_INSTALL_PATH/linux-0.11 ] || mkdir $OSLAB_INSTALL_PATH/linux-0.11
echo "\033[34mDone\033[0m"

#解压 linux-0.11
echo -n "* Extract linux-0.11......"
tar zxf common/linux-0.11.tar.gz -C $OSLAB_INSTALL_PATH/linux-0.11
cp common/linux-0.11.tar.gz $OSLAB_INSTALL_PATH
echo "\033[34mDone\033[0m"

# 解压 bochs 和 hdc image
echo -n "* Extract bochs configuration file and hdc image......"
tar zxf common/bochs-and-hdc.tar.gz -C $OSLAB_INSTALL_PATH/
echo "\033[34mDone\033[0m"

# 复制common文件夹
echo -n "* Copy common files......"
cp -r common/files $OSLAB_INSTALL_PATH
echo "\033[034mDone\033[0m"

# 判断操作位数是否为32 
if [ `getconf LONG_BIT` = "32" ]
    install_dep_i386
    install_gcc34_i386
    configure_for_i386
fi

echo "\033[34m* Installation finished (^_^)/ \033[0m"

然后将本地的LabEnv文件夹(包含该脚本以及其他的安装文件如gcc3.4、linux0.11、boch、hdc image)一同上传到ubuntu上:

可以使用ssh实现Ubuntu16.04和主机Win10之间互传文件,使用如下命令安装ssh-server

sudo apt-get install ssh openssh-server

然后用命令ifconfig查看ubuntu的IP地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZSYpHQn-1652110969608)(C:\Users\zpy\Pictures\微信截图_20220305184100.png)]

然后用Xftp进行ssh链接,用户名和密码为ubuntu的用户名和密码,IP地址为192.168.247.131。

然后使用鼠标直接拖拽即可上传成功。

4.1.3 配置Linux0.11所需实验环境

首先要切换到/LabEnv/zyw-oslab文件下执行setup脚本。

cd LabEnv/zyw-oslab
chmod 777 ./setup.sh
./setup.sh

如果直接执行./setup.sh会提示权限不够,因此需要运行chmod 777 ./setup.sh来提高文件的权限。此时再次执行ls指令会发现setup.sh 文件变为绿色,表示该脚本文件是一个可执行文件。 然后重新执行脚本文件,在/os/hit-oslab目录下输入命令./setup.sh。具体操作如下图所示,setup脚本执行成功。
在这里插入图片描述

4.2 Linux内核源码的编译和启用

4.2.1 Makefile文件分析

cdMakefile 文件的主要作用是指示make程序最终使用独立编译连接成的 tools/目录中的 build执行程序将所有内核编译代码连接和合并成一个可运行的内核映像文件 image。具体是对boot/中的bootsect.s、setup.s使用8086汇编器进行编译,分别生成各自的执行模块。再对源代码中的其他所有程序使用GNU的编译器gcc/gas进行编译,并链接模块system。最后再用 build 工具将这三块组合成一个内核映像文件image。build是由tools/build.c源程序编译而成的一个独立的执行程序,它本身并没有被编译链接到内核代码中。基本编译连接/组合结构如下图所示

在这里插入图片描述

具体的makefile代码分析如下:

#如果要使用RAM盘,就需要定义块的大小。这里默认没有定义,注释掉了-DRAMDISK=512;否则gcc编译时就会带上$(RAMDISK)的选项
RAMDISK =  #-DRAMDISK=512
#8086 汇编编译器和连接器。后带的参数含义分别 是: -0 生成 8086 目标程序; -a 生成与 gas 和gld部分兼容的代码。
AS86	=as86 -0 -a
LD86	=ld86 -0
# GNU汇编编译器和连接器
AS	=as 
LD	=ld 
#GNU链接器gld运行时用到的选项,分别是-m、-Ttext、-e
LDFLAGS	=-m elf_i386 -Ttext 0 -e startup_32
#gcc是GNU C程序的编译器。对于UNIX类的脚本而言,在引用定义的标识符时,需要在前面加上$符号并括住标识符。
CC	=gcc-3.4 -march=i386 $(RAMDISK)
#指定gcc编译器所使用的选项:-Wall打印所有的警告信息,-O2对代码进行优化,-formit-frame-pointer指明对于无需帧指针的函数不需要把帧指针(frame pointer)保存在寄存器中。
CFLAGS	=-m32 -g -Wall -O2 -fomit-frame-pointer 
#cpp是 gcc 的预处理器程序。预处理器用于进行程序中的宏替换处理、条件编译处理以及包含进指定文件的内容, 即把使用'#include' 指定的文件包含进来。源程序文件中所有以符号' #' 开始的行均需要由前处理器进行处理。程序中所有' #define' 定义的宏都会使用其定义部分替换掉。 程序中所有' #if'、'#ifdef'、'#ifndef' 和' #endif' 等条件判别行用于确定是否包含其指定范围中 的语句。
#'-nostdinc-Iinclude' 含义是不要搜索标准头文件目录中的文件, 即不用系统/usr/include/目录 下的头文件, 而是使用'-I' 选项指定的目录或者是在当前目录里搜索头文件。
CPP	=cpp -nostdinc -Iinclude
#R00T_DEV 指定在创建内核映像 (image) 文件时所使用的默认根文件系统所在的设备, 默认给的时是软盘(FLOPPY)
ROOT_DEV= #FLOPPY 
#下面是 kernel 目录、mm 目录和 fs 目录所产生的目标代码文件。为了方便引用在这里将它们用 ARCHIVES (归档文件) 标识符表示。
ARCHIVES=kernel/kernel.o mm/mm.o fs/fs.o
#块和字符设备库文件。.a表示该文件是个归档文件, 包含有许多可执行二进制代码子程序集合的库文件, 通常是用 GNU 的 ar 程序生成。 ar 是 GNU 的二进制文件处理程序, 用于创建、修改 以及从归档文件中抽取文件。
DRIVERS =kernel/blk_drv/blk_drv.a kernel/chr_drv/chr_drv.a
#数学运算库文件
MATH	=kernel/math/math.a 
#由lib/目录中的文件所编译生成的通用库文件
LIBS	=lib/lib.a
#下面是make老式的隐式后缀规则。该行指示make利用下面的命令将所有的'.c'文件编译生成'.s'汇编程序。’:'表示下面是该规则的命令。整句表示让gcc采用前面CFLAGS所指定的选项以及仅使用include/目录中的头文件,在适当地编译后不进行汇编就停止(-S),从而产生与输入的各个C文件对应的汇编语言形式的代码文件。默认情况下所产生的汇编程序文件是原C文件名去掉'.c'后再加上'.s'后缀。'-o'表示其后是输出文件的形式。其中'$*.s’(或'$@')是自动目标变量,'$<'代表第一个先决条件,这里即是符合条件'*.c'的文件。
#下面这3个不同规则分别用于不同的操作要求。若目标是.s文件,而源文件是.c文件则会使用第一个规则;若目录是.o,而原文件是.s,则使用第2个规则;若目标是.o文件而原文件是c文件,则可直接使用第3个规则。
.c.s:
	$(CC) $(CFLAGS) \
	-nostdinc -Iinclude -S -o $*.s $<
#表示将所有.s汇编程序文件编译成.o目标文件。整句表示使用gas编译器将汇编程序编译成.o目标文件。-c表示只编译或汇编,但不进行连接操作。
.s.o:
	$(AS)  -o $*.o $<
#类似上面,*.c文件-→*.o目标文件。整句表示使用gec将C语言文件编译成目标文件但不连接。
.c.o:
	$(CC) $(CFLAGS) \
	-nostdinc -Iinclude -c -o $*.o $<
#下面'all'表示创建Makefile所知的最顶层的目标。这里即是Image文件。这里生成的Image文件即是引导启动盘映像文件 bootimage。若将其写入软盘就可以使用该软盘引导Linux系统了。DOS 系统下可以使用软件rawrite.exe。
all:	Image
#说明目标(Image 文件)是由冒号后面的4个元素产生,分别是 boot/目录中的bootsect和setup文件、tools/目录中的system和 build文件。
Image: boot/bootsect boot/setup tools/system tools/build
	cp -f tools/system system.tmp
	strip system.tmp
	objcopy -O binary -R .note -R .comment system.tmp tools/kernel
	tools/build boot/bootsect boot/setup tools/kernel $(ROOT_DEV) > Image #表示使用tools目录下的build工具程序(下面会说明如何生成)将bootsect、setup和system文件以$(RO0T_DEV)为根文件系统设备组装成内核映像文件Image。
	rm system.tmp
	rm tools/kernel -f
	sync #sync同步命令是迫使缓冲块数据立即写盘并更新超级块。
#表示disk这个目标要由Image产生。dd为UNIX标准命令:复制一个文件,根据选项进行转换和格式化。bs=表示一次读/写的字节数。这里/dev/PSO是指第一个软盘驱动器(设备文件)。
disk: Image
	dd bs=8192 if=Image of=/dev/fd0 #if=表示输入的文件,of=表示输出到的文件。在 Linux系统下使用/dev/fd0。
BootImage: boot/bootsect boot/setup tools/build
	tools/build boot/bootsect boot/setup none $(ROOT_DEV) > Image
	sync
tools/build: tools/build.c #由tools目录下的build.c程序生成执行程序build。
	gcc $(CFLAGS) \
	-o tools/build tools/build.c #编译生成执行程序build的命令
boot/head.o: boot/head.s # 利用上面给出的.s.o规则生成head.o目标文件。
	gcc-3.4 -m32 -g -I./include -traditional -c boot/head.s
	mv head.o boot/
tools/system:	boot/head.o init/main.o \ #表示 tools 目录中的 system 文件要由冒号右边所列的元素生成。 
		$(ARCHIVES) $(DRIVERS) $(MATH) $(LIBS)
	$(LD) $(LDFLAGS) boot/head.o init/main.o \
	$(ARCHIVES) \
	$(DRIVERS) \
	$(MATH) \
	$(LIBS) \
	-o tools/system 
	nm tools/system | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aU] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)'| sort > System.map #System.map表示gld需要将连接映像重定向存放在System.map文件中。
#数学协处理函数文件 math. a 由 64 行上的命令实现: 进入 kernel/math/目录; 运行 make 工具程序。
kernel/math/math.a: FORCE
	(cd kernel/math; make)
#生成块设备库文件 b1k_drv.a, 其中含有可重定位目标文件。
kernel/blk_drv/blk_drv.a: FORCE
	(cd kernel/blk_drv; make)
#生成字符设备函数文件 chr_drv.a
kernel/chr_drv/chr_drv.a: FORCE
	(cd kernel/chr_drv; make)
#内核目标模块kernel.o
kernel/kernel.o: FORCE
	(cd kernel; make)
#内核管理模块mm.o
mm/mm.o: FORCE
	(cd mm; make)
#文件系统目标模块 fs.o
fs/fs.o: FORCE
	(cd fs; make)
#库函数lib.a
lib/lib.a: FORCE
	(cd lib; make)
#这里开始的三行是使用 8086 汇编和连接器 对 setup. s 文件进行编译生成 setup 文件。 -s选项表示要去除目标文件中的符号信息。
boot/setup: boot/setup.s
	$(AS86) -o boot/setup.o boot/setup.s
	$(LD86) -s -o boot/setup boot/setup.o
#生成bootsect.o磁盘引导块
boot/bootsect:	boot/bootsect.s
	$(AS86) -o boot/bootsect.o boot/bootsect.s
	$(LD86) -s -o boot/bootsect boot/bootsect.o
#bootsect.s文本程序开始处添加一行有关system模块文件长度信息,在把system模块加载到内存期间用于指明系统模块的长度。添加该行信息的方法是首先生成只含有“SYSSIZE = system文件实际长度”一行信息的tmp.s 文件,然后将bootsect.s文件添加在其后。取得system长度的方法是:首先利用命令1s对编译生成的system模块文件进行长列表显示,用grep命令取得列表行上文件字节数字段信息,并定向保存在tmp. s临时文件中。cut命令用于剪切字符串,tr用于去除行尾的回车符。其中:(实际长度+15)/16用于获得用‘节’表示的长度信息,1节=16字节。
tmp.s:	boot/bootsect.s tools/system
	(echo -n "SYSSIZE = (";ls -l tools/system | grep system \
		| cut -c25-31 | tr '\012' ' '; echo "+ 15 ) / 16") > tmp.s
	cat boot/bootsect.s >> tmp.s
#当执行'make clean'时,就会执行102-105行上的命令,去除所有编译连接生成的文件。rm'是文件删除命令,选项-f含义是忽略不存在的文件,并且不显示删除信息。
clean:
	rm -f Image System.map tmp_make core boot/bootsect boot/setup
	rm -f init/*.o tools/system tools/build boot/*.o
	(cd mm;make clean)
	(cd fs;make clean)
	(cd kernel;make clean)
	(cd lib;make clean)
#该规则将首先执行上面的clean规则,然后对1inux/目录进行压缩,生成' backup.Z'压缩文件。'cd .. '表示退到1inux/的上一级(父)目录; 'tar cf - linux'表示对1inux/目录执行ta归档程序。'-cf'表示需要创建新的归档文件‘l compress -'表示将tar程序的执行通过管道操作(’|')传递给压缩程序compress,并将压缩程序的输出存成backup.Z文件。
backup: clean
	(cd .. ; tar cf - linux | compress16 - > backup.Z)
	sync
#该目标或规则用于产生各文件之间的依赖关系。创建这些依赖关系是为了让 make 命令用它们来确定 是否需要重建一个目标对象。比如当某个头文件被改动过后, make 就能通过生成的依赖关系, 重新 编译与该头文件有关的所有*.c 文件。具体方法如下:
dep:
	sed '/\#\#\# Dependencies/q' < Makefile > tmp_make
	(for i in init/*.c;do echo -n "init/";$(CPP) -M $$i;done) >> tmp_make
	cp tmp_make Makefile
	(cd fs; make dep)
	(cd kernel; make dep)
	(cd mm; make dep)
FORCE: #强制执行
#依赖
init/main.o: init/main.c include/unistd.h include/sys/stat.h \
  include/sys/types.h include/sys/times.h include/sys/utsname.h \
  include/utime.h include/time.h include/linux/tty.h include/termios.h \
  include/linux/sched.h include/linux/head.h include/linux/fs.h \
  include/linux/mm.h include/signal.h include/asm/system.h \
  include/asm/io.h include/stddef.h include/stdarg.h include/fcntl.h

4.2.2 linux内核编译

上述的setup.sh脚本会在源目录下生成ZYWoslab文件夹, 然后进入ZYWoslab文件夹下的linux-0.11,并在该目录下使用make命令进行编译:

cd ZYWoslab/linux-0.11
make

在这里插入图片描述

等待一段时间,编译成功,如下图所示:

在这里插入图片描述

4.2.3 linux内核启用

返回上一级目录,修改run文件的权限;然后进入到bochs文件夹下,修改bochs-gdb文件夹的权限。

cd ../
chmod 777 run
cd bochs
chmod 777 bochs-gdb
cd ../
./run

在这里插入图片描述

运行run文件后,终端显示如下信息

在这里插入图片描述

同时会弹出Bochs的界面,其中“Loading system”是bootsect在加载system的时候输出的提示信息。接下来显示的是一些系统的信息如空闲的内存容量是12582912 bytes.

在这里插入图片描述

输入ls,出现下图所示说明编译成功。

在这里插入图片描述

4.2.4 测试和验证

改写 bootsect.s 主要完成如下功能:bootsect.s 能在屏幕上打印一段提示信息Hello OS world,my name is ZYW

首先修改bootsect.s的代码:

	! Hello OS world, my name is ZYW 的长度是 30,加上 6 后是 36,所以代码应该修改为 mov cx,#36。
	mov	cx,#36
	mov	bx,#0x0007		! page 0, attribute 7 (normal)
	!修改该msg1的内容
	msg1:
	.byte 13,10
	.ascii "Hello OS world, my name is ZYW"
	.byte 13,10,13,10

然后从终端进入 ~/ZYWoslab/linux-0.11/boot/ 目录。然后执行如下的两条命令实现编译和链接。

$ as86 -0 -a -o bootsect.o bootsect.s
$ ld86 -0 -s -o bootsect bootsect.o

如果这两个命令没有任何输出,说明编译与链接都通过了。Ubuntu 下用 ls -l 可列出下面的信息:

在这里插入图片描述

bootsect 的文件大小是 544 字节,而引导程序必须要正好占用一个磁盘扇区,即 512 个字节。造成多了 32 个字节的原因是 ld86 产生的是 Minix 可执行文件格式,这样的可执行文件除了文本段、数据段等部分以外,还包括一个 Minix 可执行文件头部,所以 bootsect 文件的头几个字节应该是 01 03 10 04。为了验证一下,Ubuntu 下用命令hexdump -C bootsect可以看到:

在这里插入图片描述

然后使用命令dd bs=1 if=bootsect of=Image skip=32要去掉这 32 个字节的文件头部。去掉这 32 个字节后,将生成的文件拷贝到 linux-0.11 目录下,并一定要命名为“Image”(注意大小写)

# 当前的工作路径为 /home/shiyanlou/oslab/linux-0.11/boot/

# 将刚刚生成的 Image 复制到 linux-0.11 目录下
$ cp ./Image ../Image

# 执行 oslab 目录中的 run 脚本
$ ../../run

此时bochs中成功显示了Hello OS world,my name is ZYW
在这里插入图片描述

五、实验心得

​ 通过这次实验我深入理解了linux内核的启动过程,在研读linux内核源码的过程中还深入理解了计算机底层架构和原理。比如老师上课讲过实模式和保护模式,当时对这两个的概念比较模糊。但通过这次探究linux在启动过程中从实模式到保护模式的转变,更加清楚的认识到了这两种模式的区别,以及在两种不同模式下中断方式的不同。在bootsect的研读过程中,我非常佩服操作系统的设计者,因为他们非常全面地、整体地考虑内存的规划,内存的规划都是经过精密测算的。在研读setup.s的程序中,system模块复制到0x00000这个动作我觉得设计的非常的巧妙,它废除了BIOS的中断向量表,也就是废除了16位的中断机制,为建立32位的操作系统的中断机制做铺垫。同时通过setup中对中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置,还加深了老师课堂上提过的描述符表的理解。

​ linux0.11的代码量相对与其他的版本较小,易于入门学习,但是我在研读的过程中也遇到了很多难以理解的地方,通过翻阅赵炯博士的解析,得以一点点的理解,这个过程是很枯燥的,但同时也收获颇丰。最后我通过实际的改写一些脚本和代码,然后编译和启动,最终验证成功,锻炼了我的动手操作能力,加深了对源码的理解。

六、参考

【1】https://www.lanqiao.cn/courses/115

【2】《Linux内核设计的艺术:图解Linux操作系统架构设计与实现原理(第2版)》

【3】《Linux内核完全注释》

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zyw2002/article/details/124678314

小程序商城框架源码 小程序源码带前端+后台+数据库-爱代码爱编程

小程序商城框架源码 小程序源码带前端+后台+数据库    开发语言:php5.4+  (框架)Thinkphp5 开发工具:phpstrom 数据库:mysql 安装指导: 1.此处使用Linux系统+宝塔面板作为操作演示; 2.上传项目源码,并解压到网站目录; 3.修改数据库配置文件; 4.访问项目地址:http://你的域名/ind

这9个Linux工具值得关注-爱代码爱编程

有很多干扰和非生产性活动会影响我们在工作场所的表现,还有很多方法可以提高注意力和工作效率。如果您正在寻找一种方法来提高工作效率并保持井井有条,请考虑使用特殊软件来创建高效的工作环境。 我们收集了一份您可能没有听说过的适用于 Linux 平台的生产力工具列表,他们将帮助您: 阻止干扰;跟踪您如何度过工作时间;使手工工作自动化;提醒重要的待办事项;组织和构

解决虚拟机突然连不上网的问题-爱代码爱编程

在之前很久就遇到过这个问题,虚拟机经常会莫名其妙连不上网。 容易出现的报错例如: Name or service not known 如何得知这是虚拟机联网的问题呢? 使用虚拟机自带火狐浏览器或者在虚拟机控制台输入以下语句: ping www.baidu.com 解决办法: 使用链接中的方案一解决过这个问题:Vmware虚拟机

Linux常用命令整理—进程-爱代码爱编程

0.1查看当前系统进程状态 ps:查看当前系统中正在执行的各种进程的信息 [root@hadoop100 ~]# ps PID TTY TIME CMD 3232 pts/0 00:00:00 bash 3940 pts/0 00:00:00 ps 常用方式1:ps aux 参数说明: a:显示当前终

Parallels虚拟机Linux和Mac之间共享文件夹-爱代码爱编程

目录   环境清单 共享文件的方法 第一种:scp命令 第二种:共享文件夹 环境清单 Macbook Air M1,Mac OS 12.3 Parallels虚拟机软件 Ubuntu 20.04.3 共享文件的方法 第一种:scp命令 scp filename usrname@IP:targetdirectory 可以结合我之

7.嵌入式控制器ec实战 acpi规范中的电源管理通道pmc(power management channel)_acpi ec_编程分享的博客-爱代码爱编程

文章目录 前言 一、电源管理通道PMC概述 二、通过电源管理通道PMC完成EC和BIOS或操作系统通信 操作系统或BIOS通过PMC通道与EC通信

ubuntu系统国内代理源设置_ubuntu source list proxy-爱代码爱编程

默认源配置在国外,在国内下载会相对较慢。可以设置为如下国内的源进行加速。 但是需要注意的是,有些软件的国内源可能会存在版本的问题,需要留意相关情况。笔主遇到过18版本阿里源安装gcc的版本问题。 1、备份原有设置参数 c

cent os7基础 第八节_cent os 7.8-爱代码爱编程

ps:查看静态的进程统计信息 top:查看动态的进程排名信息 pgrep命令:根据特定条件查询进程 PID 信息 pstree命令:以树形结构列出进程信息 进程的前后台调度: 1>Ctrl+Z 组合键:将当前

docker容器网卡eth0@ifx浅析_docker ifconfig eth0 up 失败-爱代码爱编程

添加veth-pair对 # 查看此时网络设备 [root@boy ~]# ip link show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue

linux多线程概念_linux多线程是什么意思-爱代码爱编程

线程的概念:在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更细更轻量化 现在出现了很多新的问题,什么叫做进程内部?什么是执行分支?为什么属于进程的一部分? 下面通过画图理解的方式来解决上述的问题 目录 一、多线程理解 二、Linux线程的特点 1、创建过程 2、CPU调度 3、资源承担的实体 4、从图的角度理解

【linux】rhce备考复习文档编辑grep命令-爱代码爱编程

grep命令 grep来自于英文词组“global search regular expression and print out the line”的缩写,意思是用于全面搜索的正则表达式,并将结果输出。人们通常会将gre

离线安装docker(正式版)_docker 离线版本-爱代码爱编程

写在前面 之前写过一篇关于docker安装的博客,那种方式安装有很多缺点。运行docker和使用docker的时候会产生多个进程,占用Linux主机的资源。于是,我找到了新的方式安装docker。 重要的三个文件

【linux】多路转接之poll_revents-爱代码爱编程

文章目录 poll接口pollfd结构timeout决定poll的等待策略 poll TCP服务器执行流程PollServer实现poll的优点poll缺点 poll也是 一种多路转接

如何成为一名高级数字 ic 设计工程师(3-爱代码爱编程

如何成为一名高级数字 IC 设计工程师(3-1)工具篇:Linux Shell 技术 版权所有,新芯设计,转载文章,请注来源 引言 🌏 一、简单的返回、定位、命名、提交、复制、粘贴、移动、删除等操作 🌏 二、简单的查找、查看、排序、比较等操作 🌏 三、简单的搜索、匹配、替换、过滤等操作 🌏 四、简单的显示信息、环境变量、执行权限、

k8s拉取初始化镜像_the image 'registry.cn-爱代码爱编程

#!/bin/bash images=( kube-apiserver:v1.24.0 kube-proxy:v1.24.0 kube-controller-manager:v1.24.0 kube-scheduler:v1.24.0 etcd:3.5.3-0 pause:3.7 ) for imageN