SkyTianChi 发布的文章

linker脚本文件是用来控制link过程的文件,文件中包含内容为linker的处理命令,主要用于描述输入文件到输出文件(目标文件)时各个内容的的分布及内存映射等等。在上一节中的linker.ld已经告诉了链接器把需要初始化的部分放在_start_ctors和_end_ctors之间,关于链接脚本格式还有一些其他内容需要指定。
linker脚本最简单的格式为

SECTIONS
{
    . = 0x10000;
    .text : { *(.text) }
    . = 0x8000000;
    .data : { *(.data) }
    .bss : { *(.bss) }
}

.text是输出节。在大括号里列出输入节的名字,它们会放入输出节中。使用通配符匹配任何文件名,表达式(.text)意味着所有的输入文件中的输入节.text。表示通配符,.是位置计数器,它以输出节的大小增加其值,链接器会设置输出文件中的.text节的地址为0x10000。
剩下的行定义输出文件中的.data和.bss节。链接器会把输出节.data放置到地址0x8000000。之后,链接器把输出节.data的大小加到位置计数器的值0x8000000,并立即设置.bss输出节,效果是在内存中,.bss节会紧随.data之后(两个节之间可能产生必要的对齐)。
对于一个被抛弃的的输出段,链接器将会忽略给该段指定的地址,除非在输出段中有符号定义,那样的话,即使这个输出段被抛弃,链接器依然会遵守该段地址的指定。一个特殊的输出段名/DSICARD/可以用来给抛弃输入段使用,任何被放在名字为/DISCARD/输出段中的输入段都不会被包含到输出文件中。

1.5 操作系统的引导过程

有了linker.ld脚本文件,就可以执行链接的过程。在Makefile中加入链接过程。

%.o: %.s
    as ${ASPARAMS} -o $@ $<

mykernel.bin: linker.ld ${objects}
    ld ${LDPARAMS} -T $< -o $@ ${objects}

我们使用VMware Workstation启动操作系统,VMware Workstation的测试版本为16.0.0,它使用操作系统镜像启动一个操作系统。Linux有制作镜像的工具,在命令行中使用下面命令安装:

sudo apt install xorriso grub-efi-amd64 grub-pc

sudo apt install xorriso grub-efi-amd64 grub-pc
安装完毕以后可以使用指令grub-mkrescue创建iso镜像
//这里就是可以让iso镜像自动更新

set timeout=0
set default=0
menuentry "my os" {
    multiboot /boot/mykernel.bin
    boot
}

Multiboot规范规定GRUB根据/boot/grub/grub.conf文件查找Kernel信息并加载Kernel程序,grub.conf的信息如下:

set timeout=0
set default=0
menuentry "my os" {
    multiboot /boot/mykernel.bin
    boot
}

我们在Makefile文件中创建这个文件,添加代码如下:

mykernel.bin: linker.ld ${objects}
    ld ${LDPARAMS} -T $< -o $@ ${objects}

mykernel.iso: mykernel.bin
    mkdir iso
    mkdir iso/boot
    mkdir iso/boot/grub
    cp $< iso/boot/
    echo 'set timeout=0' > iso/boot/grub/grub.cfg
    echo 'set default=0' >> iso/boot/grub/grub.cfg
    echo 'menuentry "my os" {' >> iso/boot/grub/grub.cfg
    echo '    multiboot /boot/mykernel.bin' >> iso/boot/grub/grub.cfg
    echo '    boot' >> iso/boot/grub/grub.cfg
    echo '}' >> iso/boot/grub/grub.cfg
    grub-mkrescue --output=$@ iso
    rm -rf iso

clean:
    rm kernel.o loader.o mykernel.bin mykernel.iso
    //当.s文件由更改时,必须重新编译生成.o文件,然后重新链接,生成.iso

在命令行中执行

make mykernel.bin

可以看到在os目录下生成了mykernel.iso文件。
打开VMware Workstation并选择“文件 - 新建虚拟机 - 典型 - 安装程序光盘映像文件 - 选择镜像文件所在的路径”。操作系统类型更改为“其他”,内存分配64M,不需要创建磁盘。创建完毕后开启虚拟机可以看到屏幕上的hello world。
c48c175774bfe30d891a554bf526b43.png

printf的一个BUG是每次打印字符都从固定的位置0xb8000开始,下一次打印将覆盖上一次的内容。

unsigned short *VideoMemory = (unsigned short*)0xb8000;

屏幕的大小是80 x 25,每输出80个字节需要进行换行,每满25行需要清屏,
打印位置重置为第一行行首。
然而屏幕在内存中的地址是连续的,
我们可以给光标增加一个X Y
而因为char为一个字节,所以我们可以直接通过x++来进行光标的移动。
因此可以用VideoMemory[80 * y + x]来访问第y行第x位。如果遇到换行符,则直接进行换行。

void printf(const char *str)
{
    // screen address
    static uint16_t *VideoMemory = (uint16_t*)0xb8000;
    static uint8_t x = 0, y = 0;

    for (int i = 0; str[i]; i++)
    {
        switch (str[i])
        {
            case '\n':
                y++;
                x = 0;
                break;
            default:
                VideoMemory[80 * y + x] = (VideoMemory[80 * y + x] & 0xFF00) | str[i];
                x++;
        }

        if (x >= 80)
        {
            x = 0;
            y++;
        }
        if (y >= 25)
        {
            for (y = 0; y < 25; y++)
            {
                for (x = 0; x < 80; x++)
                    VideoMemory[i] = (VideoMemory[i] & 0xFF00) | ' ';
            }
            x = 0;
            y = 0;
        }
    }
}

GNU GRUB

GNU GRUB(GRand Unified Bootloader, GRUB) 是一个来自GNU项目的多操作系统启动程序。GRUB是多启动规范的实现,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。
使用GRUB引导操作系统的过程为:

image.png

  1. BIOS转向第1块硬盘的第1个扇区,即 主引导记录(MBR)

BIOS系统存储于主板的ROM芯片上,计算机开机时,先读取该系统,然后进行加电自检。这个过程中检查CPU和内存、计算机基本的组成单元(控制器、运算器和存储器),以及检查其他硬件,若没有异常就开始加载BIOS程序到内存当中.

  1. MBR中存储了BootLoader信息,BootLoader将加载GRUB;
  2. MBR的工作是查找并加载第二段BootLoader程序。但在系统没启动时,MBR无法识别文件系统,因此需要在这一步加载GRUB。
  3. GRUB查找并加载kernel;GRUB识别文件系统,根据/boot/grub/grub.conf文件查找Kernel信息并加载Kernel程序。当Kernel程序被检测并加载到内存中,GRUB就将控制权交接给Kernel程序。
  4. kernel装载驱动,挂载ROOTFS,执行/sbin/init;
  5. init初始化os,执行runlevel相关程序。

(现代操作系统使用的UEFI启动)

使用Multiboot规范编写loader.s

BootLoader是一段汇编代码,文件名为loader.s,它需要按照Multiboot规范来编译内核才可以被GRUB引导。
按照Mutileboot规范,内核必须在起始的8KB中的包含一个多引导项头(Multiboot header),里面必须包含3个4字节对齐的块:这个多引导项头里面必须有3个4字节对齐的块。

一个魔术块:包含了魔数[0x1BADB002],是多引导项头结构的定义值。
一个标志块:我们不关心这个块的内容,我们简单设定为0。
一个校检块:校检块,魔术块和标志块的数值的总和必须是0。

我们在loader.s中定义他们
image.png

.set MAGIC, 0x1badb002;                 # 魔数块
.set FLAGS, (1<<0 | 1<<1);              # 标志块<br />.set CHECKSUM, -(MAGIC + FLAGS);        # 校验块

下面的伪指令声明了Multiboot标准中的多引导项头
三个块都是32位字段

.section .multiboot
.long MAGIC//long即是4字节
.long FLAGS
.long CHECKSUM

在多引导项头之后,是程序的入口点。kernel的代码在文件kernel.cpp中,loader.s在入口点中跳转到kernel.cpp的函数中执行。首先需要使用.global伪指令告诉链接器程序的入口点,用loader表示,最后是stop代码。

kernel.cpp中主函数的名称是kernelMain,需要传入两个参数,分别是BootLoader的地址和魔数,它们存放于寄存器%eax和%ebx中,将它们压栈以传递参数。kernelMain是一个外部符号,需要提前在loader.s中声明,这些外部符号存在于.text段中。同样地,为了使得loader函数对外部可见,使用.global伪指令向外暴露loader。

.section .multiboot
    # ...
    .long CHECKSUM

.section .text
.extern kernelMain
.global loader

loader:
    push %eax       # bootloader's address in %eax
    push %ebx       # magic number in %ebx
    call kernelMain

stop:
    # 禁用中断
    cli
    # 禁用中断后使用hlt暂停CPU,以后无法再唤醒
    hlt
    jmp stop

一:Makefile变量

1、赋值符号“=”
20191024153840343.png2019102415393259.png
实验结果显示被“=”赋值的变量,其值取决于最后一次赋值。
指令“print”中echo前加上“@”和省略其命令执行过程,结果如下:
2、赋值符“:=”
201910241523342.png20191024152404104.png
3、赋值符“?=”
image.png image.png
如果“name”已经被赋值则用之前的值“zhao”,否则用“li”
4、变量追加“+=”

  ![20191024153541221.png](https://cdn.nlark.com/yuque/0/2022/png/26139547/1653748183188-0134cae8-1d27-405f-a719-182e3f5eb54a.png#clientId=uf74e8550-4887-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u6307b019&margin=%5Bobject%20Object%5D&name=20191024153541221.png&originHeight=150&originWidth=300&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10186&status=done&style=none&taskId=u710b533d-9bb6-4ffe-a23c-2c76aa2a6ff&title=)![20191024153619751.png](https://cdn.nlark.com/yuque/0/2022/png/26139547/1653748187431-bfa450d3-a896-46b4-a943-d49f0be974d6.png#clientId=uf74e8550-4887-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ub8f9edbd&margin=%5Bobject%20Object%5D&name=20191024153619751.png&originHeight=150&originWidth=300&originalType=binary&ratio=1&rotation=0&showTitle=false&size=13196&status=done&style=none&taskId=u6e2ee99c-c558-4a34-af35-d397bd2eb1e&title=)

在变量“name”之后追加“wang”

三:Makefile模式规则
a.o : a.c
gcc -c a.c
b.o : b.c
gcc -c b.c
运行模式规则“%”:当目标中重现“%”时,目标中“%”所代表的值决定了依赖文件中的“%”的值

%.o : %.c
gcc -c $<
四、Makefile伪目标
伪目标主要是为了避免Makefile中定义的执行指令和工作目录下的实际文件出现名字冲突。

举例说明:当前目录下如果有一个名为“clean”的文件,执行make clean指令,因为没有依赖文件,所以后续的rm指令不会被执行。解决方法为在Makefiel中将指令声明为伪目标即可“.PHONY”

.PHONY

clean:
rm *.o
... ...
五、Makefile函数
1、函数“subst”:完成字符串替换

$(subst <from>, <to>, <text>)<br /> <br />$(subst aaa, AAA, 3a transform 3A aaa)
将字符串“3a transform 3A aaa ”中的“aaa”替换为“AAA”即:“3a transform 3A AAA”

2、函数“patsubst”:完成模式字符串替换

$(patsubst <pattern>, <replacement>, <text>)<br /> <br />$(patsubst %.c, %.o, a.c b.c c.c)
将字符串“a.c b.c c.c”替换为“a.o b.o c.o”

如果text = a.c b.c c.c

那么,“$(text: .c = .o)”等同于“$(patsubst %.c, %.o, $(text))”

3、函数“dir”:获取目录

$(dir <name...>)<br /> <br />$(dir </src/a.c>)
提取文件“/src/a.c”的目录部分“/src”

4、函数“notdir”:提取目录名

$(notdir <name...>)<br /> <br />$(notdir <src/a.c>)
提取文件“/src/a.c”的非目录部分“a.c”

5、函数“foreach”:完成循环

6、函数“wildcard”:在非规则模式下即变量定义和函数中等同于“%”通配符,将相应对象展开

$(foreach <var>, <list>, <text>)

SRCDIRS := dira dirb dirc
$(foreach dir, $(SRCDIRS), $(wildcard $(dir) / *.c))
循环将SRCDIRS中的各个目录放进dir变量中,调用wildcard函数提取dir目录下所有.c文件

六、Makefile自动化变量

20191024163707803.png

一:虚拟机选择

我选择的VSCODE+VMware虚拟机装载LINUX UBUNTU20.04系统
ubuntu20.04下载网站:http://mirrors.163.com/ubuntu-releases/20.04/
创建虚拟机:
打开vmware,选择【文件】——【新建虚拟机】:典型
image.png
之后的ubuntu汉化+更换国内源节点会再补充

二:编译软件选择

VSCODE体积小,编辑快,且可以通过SSH连接虚拟机。
下载编译器:https://code.visualstudio.com/
1.安装插件
image.png
安装SSH所需插件还有C语言所需插件,通过SSH配置文件连接

Host tich
  HostName 192.168.5.10
  User skytianchi
  Port 22

HOST指的是连接者的名字
HOSTNAME是IP地址
IP地址查询
image.png
ifconfig查询IP地址,可以看到我的虚拟机IP地址为192.168.5.10
如果嫌麻烦,可以在代码后面加一行Password:xxxxxx便可以每次自动连接

三:学习MAKEFILE文件的编写

makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,
UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)(拥有了虚拟地址)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link) (整合为一个文件)。
make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。
​1.如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2.如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3.如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。
只要我们的Makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。
而编写MAKEFILE文件就意味着我们可以定向编译文件。

四:Makefile的规则

target... : prerequisites ...
command
...
...
-------------------------------------------------------------------------------

target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。
prerequisites就是,要生成那个target所需要的文件或是目标。
command也就是make需要执行的命令。(任意的Shell命令)
https://blog.csdn.net/weixin_38391755/article/details/80380786

GPPARAMS = -m32 -fno-user-cxa-atexit-fleading-underscore -fno-exception -fno
ASPARAMS = --32
LDPARAMS = -melf_i386

objects = loader.o kernel.o

%0 : %.cpp
    g++ ${GPPRAMS} -o $@ -c $<

%.0: %.s
    as ${ASPAMS} -o $@ $<

mykernel.bin: linker.ld $(objects)
    ld ${LDPARAMS} -T $< -o $@ ${objects}

install:kernel.bin
    sudo cp $< /boot/kernel.bin

五:.s文件的编写

一、大小写后缀的区别
.s 汇编语言源程序;汇编
.S 汇编语言源程序;预处理,汇编

小写的s文件,在后期阶段不在进行预处理操作,所以我们不能在这里面写预处理的语句在里面
大写的S文件,还会进行预处理、汇编等操作,所以我们可以在这里面加入预处理的命令

二、编译的相关流程

预处理(Pre-Processing)-->编译(Compiling)-->汇编(Assembling)-->链接(Linking)

1、预处理
​ 根据以字符#开头的命令(directives),修改原始的C程序
​ 这个阶段并不会去检查代码的错误,只会把#的语句转成C代码
​[gan@localhost gcc]# gcc E hello.c o hello.i

2、编译阶段
在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。汇编语言是非常有用的,它为不同高级语言不同编译器提供了通用的语言。如:C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
​[gan@localhost gcc]# gcc S hello.i o hello.s

(3)汇编阶段
汇编阶段是把编译阶段生成的”.s”文件转成目标文件,读者在此可使用选项”-c”就可看到汇编代码已转化为”.o”的二进制目标代码了。如下所示:
[gan@localhost gcc]# gcc c hello.s(小写s) o hello.o

(4)链接阶段
将库函数(头文件中用到的)等链接到目标文件中
在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库
"stdio.h"中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现”printf”这些库函数的呢?最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf” 了,而这也就是链接的作用。
函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。
完成了链接之后,gcc就可以生成可执行文件

六:Multiboot规范

每个操作系统都有自己的引导程序,LINUX立了一个引导程序的规范叫做Multiboot规范
Multiboot头的布局必须如下:

偏移 类型 域名 注意

0 u32 magic 要求

4 u32 lags 要求

8 u32 checksum 要求

12 u32 header_addr 如果设置了flags[16]

16 u32 load_addr 如果设置了flags[16]

20 u32 load_end_addr 如果设置了flags[16]

24 u32 bss_end_addr 如果设置了flags[16]

28 u32 entry_addr 如果设置了flags[16]

32 u32 mode_type 如果设置了flags[2]

36 u32 width 如果设置了flags[2]

40 u32 height 如果设置了flags[2]

44 u32 depth 如果设置了flags[2]
magicnumber 魔法数字这可能就是程序员的浪漫吧