打卡第二天:学习HelloOS源码

本文详细介绍了makefile的构建规则和宏定义,通过实例解析了其工作原理。同时,讲解了LD在链接过程中如何合并目标文件、重定位地址和处理符号引用。此外,还探讨了操作系统引导汇编代码的作用。最后,概述了HelloOS的编译和安装过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(2021年10月29日打卡第二天)



1、make

下面来通过一个简单的makefile实例,来看看怎么写好一个makefile:

#makefile实例1
helloworld : helloworld.c
	gcc -o $@ $<

这就是一个最简单的makefile。其中,构建目标为helloworld,目标所依赖的文件是helloworld.c,它们之间用“:”隔开。
要执行的命令是gcc -o $@ $<$@ $<是make程序内部使用的两个变量,分别代表目标及目标所依赖的文件,也就是说make程序最终会把gcc -o helloworld helloworld.c送到终端里去执行。

  • 注意make规定要执行的命令的前面必有一个tab符。

再来看一个有点复杂的makefile:

#makefile实例2
CC = gcc #定义一个宏CC 等于gcc
CFLAGS = -c #定义一个宏 CFLAGS 等于-c
OBJS_FILE = file.o file1.o file2.o file3.o file4.o #定义一个宏
.PHONY : all everything #定义两个伪目标all、everything
all:everything #伪目标all依赖于伪目标everything
everything :$(OBJS_FILE) #伪目标everything依赖于OBJS_FILE,而OBJS_FILE是宏会被
#替换成file.o file1.o file2.o file3.o file4.o
%.o : %.c
   $(CC) $(CFLAGS) -o $@ $<

我来解释一下这个例子:make 规定“#”后面为注释,make 处理 makefile 时会自动丢弃。
makefile 中可以定义宏,方法是在一个字符串后跟一个“=”或者“:=”符号,引用宏时要用“ ( 宏 名 ) ” , 宏 最 终 会 在 宏 出 现 的 地 方 替 换 成 相 应 的 字 符 串 , 例 如 : (宏名)”,宏最终会在宏出现的地方替换成相应的字符串,例如: ()(CC) 会被替换成 gcc,$( OBJS_FILE) 会被替换成 file.o file1.o file2.o file3.o file4.o。

.PHONY 在 makefile 中表示定义伪目标。所谓伪目标,就是它不代表一个真正的文件名,在执行 make 时可以指定这个目标来执行其所在规则定义的命令。但是伪目标可以依赖于另一个伪目标或者文件,例如:all 依赖于 everything,everything 最终依赖于 file.c file1.c file2.c file3.c file4.c。
虽然我们会发现,everything 下面并没有相关的执行命令,但是下面有个通用规则:“%.o : %.c”。其中的“%”表示通配符,表示所有以“.o”结尾的文件依赖于所有以“.c”结尾的文件。

例如:file.c、file1.c、file2.c、file3.c、file4.c,通过这个通用规则会自动转换为依赖关系:file.o: file.c、file1.o: file1.c、file2.o: file2.c、file3.o: file3.c、file4.o: file4.c。
然后,针对这些依赖关系,分别会执行:$(CC) $(CFLAGS) -o $@ $< 命令,当然最终会转换为:gcc –c –o xxxx.o xxxx.c,这里的“xxxx”表示一个具体的文件名。

现在来把上面的那个makefile稍稍改进一下,如果细心观察不难发现上面的makefile一共有4大部分:
构建时用到的命令、依赖文件、构建目标、构建规则。
首先建立一个文件cmd.mh,文件名随意。用于存放相关的命令,文件内容:

CC = gcc		#定义一个宏 CC 等于gcc
CFLAGS = -c		#定义一个宏 CFLAGS 等于-c

实际中或许有更多的命令,这里为了简单起见,只有一个命令和一个命令选项。
再建立一个文件objs.mh,文件名随意。用于存放需要编译的文件即依赖文件,文件内容:

OBJS_FILE = file.c file1.c file2.c file3.c file4.c	#定义一个宏
#当然可以有更多的文件

然后建立一个文件rule.mh,文件名随意。存放用于构建目标的通用规则,文件内容:

%.o : %.c
$(CC) $(CFLAGS) -o $@ $<
#也许有更多的规则例如:
#%.o : %.s
#	$(AS)	$(ASFLAGS) -O $@ $<

最后在任何情况下,我们可能就只需建立这样的makefile:

#makefile实例3
include cmd.mh
include objs.mh
.PHONY : all everything
all:everything
everything :$(OBJS_FILE)
include rule.mh

上面makefile中的include和C语言中的include是一样的工作机制,同样也是包含一个文本文件,只是make不需要 “<>” 和 “""”。并且make会在include的地方开始展开包含的这个文件。也就是说上面这个makefile最终会变成和前面代码清单makefile实例2中的那个makefile一样。
这样写起makefile文件就容易多了,有时甚至只需要改变everything : 后面的这个宏就可以了,这对于管理构建操作内核模块文件非常有效。

小结:通过三个简单的例子,大致了解了make的工作机制,无非就是层层替换,然后根据目标的依赖文件是否已更新,决定是否重新构建目标,最后执行构建目标的命令。

2、LD

LD其实就是把诸多GCC生成的可链接的目标文件合并成一个大的可执行文件,主要完成如下功能:
1)把每个目标文件中各个相同的段,合并成各个相同的大段,一个目标文件中通常默认情况下至少有三个段:.text段即代码段,.data段即已初始化的数据段,.bss段即未初始化或初始化为0的段。
2)重定位程序中的地址,完成装载程序运行时的地址绑定,一般程序都必须从某一固定的地址开始运行。
3)解决不同目标文件之间符号引用的关系,例如,A目标文件中的一个函数调用了B目标文件中的一个函数。
现在去研究LD的链接脚本,这个链接脚本才是LD真正强大的地方。
什么是链接脚本呢,简单地说就是一个文本文件,它里面存放了一些信息,这些信息有一定的规则,相当于一种编程语言,LD读取这个文件里的信息,然后根据这些信息完成相关的动作。

先来看一看一个非常简单的LD链接脚本:

/*彭东 @ 2021.01.09*/

ENTRY(_start)
/*ENTRY(),表示程序中某一标号为可执行文件的入口点,即表示程序从这里开始运行,如_start标号。*/
OUTPUT_ARCH(i386)
OUTPUT_FORMAT(elf32-i386)
/*SECTIONS,表示定义一个输出端,内部按照一定的规则组织合并所有可链接目标文件内的所有段,最后形成一个输出的大段。以一对“{}”结束。*/
SECTIONS
{
	. = 0x200000;
	/*“.”表示当前地址计数器,LD创建程序加载运行地址时,就是以这个基准的最开始应该给初始值,LD就是以那个初始值为地址开始链接程序的。*/
	__begin_start_text = .;
	.start.text : ALIGN(4) { *(.start.text) }
	__end_start_text = .;

	__begin_text = .;
	.text : ALIGN(4) { *(.text) }
	__end_text = .;

	__begin_data = .;
	.data : ALIGN(4) { *(.data) }
	__end_data = .;

	__begin_rodata = .;
	.rodata : ALIGN(4) { *(.rodata) *(.rodata.*) }
	__end_rodata = .;

	__begin_kstrtab = .;
	.kstrtab : ALIGN(4) { *(.kstrtab) }
	__end_kstrtab = .;

	__begin_bss = .;
       .bss : ALIGN(4) { *(.bss) }
	__end_bss = .;
}

上面LD链接脚本中的内容,我们要关注的内容如下:

  1. ENTRY(),表示程序中某一标号为可执行文件的入口点,即表示程序从这里开始运行,如_start标号。
  2. OUTPUT_ARCH(),表示输入可执行文件的机器体系,如ARM。
  3. OUTPUT_FORMAT(),表示输出可执行文件的格式,如“elf32-littlearm”。
  4. “.”表示当前地址计数器,LD创建程序加载运行地址时,就是以这个基准的最开始应该给初始值,LD就是以那个初始值为地址开始链接程序的。
  5. 如果定义了这些自定义的标号,就可以在程序代码中访问它们,它们的值表现为地址,例如,__begin_start_text的值为0x200000,这些类似C语言的赋值表达式要以分号“;”结束。
  6. “.text : ALIGN(4) { *(.text) }”,”其中的ALIGN(4)表示每个大段链接时的地址按4B对齐,ALIGN(4)不是必须的。ALIGN(4)前面的.text、.data、.bss,表示LD合成可执行文件时将会输出大段的段名,大段名后面要以“:”开始,紧跟其后的时“{}”,“{}”里面时链接规则。
    例如,“{*(.text)}”表示将所有目标文件中的.text段(用“()”括起来)组成一个大的.text段,其中的“*”表示通配符,当然可以在此写上所有目标文件的名称。

3、Hello OS 引导汇编代码

entry.asm:
在这里插入图片描述
在这里插入图片描述

以上的汇编代码(/lesson01/HelloOS/entry.asm)分为 4 个部分:

  1. 代码 1~40 行,用汇编定义的 GRUB 的多引导协议头,其实就是一定格式的数据,我们的 Hello OS 是用 GRUB 引导的,当然要遵循 GRUB 的多引导协议标准,让 GRUB 能识别我们的 Hello OS。之所以有两个引导头,是为了兼容 GRUB1 和 GRUB2。
  2. 代码 44~52 行,关掉中断,设定 CPU 的工作模式。你现在可能不懂,没事儿,后面 CPU 相关的课程我们会专门再研究它。
  3. 代码 54~73行,初始化 CPU 的寄存器和 C 语言的运行环境。
  4. 代码 78~87 行,GDT_START 开始的,是 CPU 工作模式所需要的数据,同样,后面讲 CPU 时会专门介绍。

4、Hello OS 的函数

main.c:
main.c

vgastr.h:
vgastr.h
vgastr.c:
在这里插入图片描述


5、编译

下面我们用一张图来描述我们 Hello OS 的编译过程,如下所示:
在这里插入图片描述

进入linux类操作系统,在源码lesson02目录空白处右击鼠标,打开一个命令窗口,输入命令make回车即可:
在这里插入图片描述

6、安装 Hello OS

经过上述流程,我们就会得到 Hello OS.bin 文件,但是我们还要让 GRUB 能够找到它,才能在计算机启动时加载它。这个过程我们称为安装,不过这里没有写安装程序,得我们手动来做。
经研究发现,GRUB 在启动时会加载一个 grub.cfg 的文本文件,根据其中的内容执行相应的操作,其中一部分内容就是启动项。
GRUB 首先会显示启动项到屏幕,然后让我们选择启动项,最后 GRUB 根据启动项对应的信息,加载 OS 文件到内存。

具体操作过程,请看:
打卡第一天:通过GRUB启动我们自己的操作系统HelloOS
https://blog.youkuaiyun.com/zhaopeng01zp/article/details/121027198

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值