最全的Linux教程,Linux从入门到精通
======================
-
linux从入门到精通(第2版)
-
Linux系统移植
-
Linux驱动开发入门与实战
-
LINUX 系统移植 第2版
-
Linux开源网络全栈详解 从DPDK到OpenFlow
第一份《Linux从入门到精通》466页
====================
内容简介
====
本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。
本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。
需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
因此,编译器对.s和.c文件,执行编译与链接后,程序就可以从该入口点开始执行。
通常入口点,就在汇编文件中,这里就是mentry.S。
我们看,链接脚本riscv-pk\bbl\bbl.lds,如下:
ENTRY(symbol),这里的symbol指的是符号表中的符号。汇编阶段会生成符号表,符号表中的符号,包括静态变量、全局变量、函数名等。这是将某一个符号symbol的值,设为入口地址。
我们再看,riscv-pk\machine\mentry.S,如下:
因此bbl.lds指定的入口点,就是mentry.S中的reset_vector处。
即0x80000000处,指令为j do_reset
。
2.2 执行mentry.S
执行指令j do_reset
,跳转到do_reset
地址后,后续执行过程,如下所示:
汇编代码添加了注释,比较简单,主要有以下操作:
- 主要是初始化x系列寄存器,以及根据misa寄存器判断;
- 此外,前面讲过的a0(mhartid)与a1(设备树)寄存器值,作为init_first_hart函数的入参;
- 保存异常向量表基址
trap_vector
,到mtvec寄存器; - 准备栈指针sp;
- 最后,跳转到riscv-pk\machine\minit.c中的init_first_hart函数,进入到C代码阶段。
2.3 执行init_first_hart函数
init_first_hart函数,如下所示:
void init\_first\_hart(uintptr_t hartid, uintptr_t dtb)
{
// 解析device tree,进行一系列初始化
query\_uart(dtb);
...
query\_chosen(dtb);
// 初始化hart, hls, 内外部中断, 内存等
hart\_init();
hls\_init(0); // this might get called again from parse\_config\_string
wake\_harts();
plic\_init();
hart\_plic\_init();
//prci\_test();
memory\_init();
boot\_loader(dtb);
}
主要是,解析设备树,进行一系列的初始化操作,具体我们暂时不关心。我们只对启动流程关键信息,做介绍,其他忽略。
在query_chosen函数,会从设备树中,解析出kernel_start和kernel_end,如下:
void query\_chosen(uintptr_t fdt)
{
struct fdt\_cb cb;
fdt\_scan(fdt, &cb);
kernel_start = chosen.kernel_start;
kernel_end = chosen.kernel_end;
}
经过如下函数,依次将kernel_start传参,如下:
void boot\_loader(uintptr_t dtb)
{
/\* Use optional FDT preloaded external payload if present \*/
entry_point = kernel_start ? kernel_start : &_payload_start;
boot\_other\_hart(0);
}
void boot\_other\_hart(uintptr_t unused \_\_attribute\_\_((unused)))
{
entry = entry_point;
enter\_supervisor\_mode(entry, hartid, dtb\_output());
}
2.4 执行enter_supervisor_mode函数
在enter_supervisor_mode函数中,就是进入S模式前,做的一些操作,为启动kernel做准备,如下所示:
void enter\_supervisor\_mode(void (\*fn)(uintptr_t), uintptr_t arg0, uintptr_t arg1)
{
setup\_pmp();
uintptr_t mstatus = read\_csr(mstatus); // 读取mstatus
mstatus = INSERT\_FIELD(mstatus, MSTATUS_MPP, PRV_S); // mstatus.MPP = PRV\_S
mstatus = INSERT\_FIELD(mstatus, MSTATUS_MPIE, 0); // mstatus.MPIE = 0
write\_csr(mstatus, mstatus); // 写入mstatus
write\_csr(mscratch, MACHINE\_STACK\_TOP() - MENTRY_FRAME_SIZE); // 写入mscratch
#ifndef \_\_riscv\_flen
uintptr_t \*p_fcsr = MACHINE\_STACK\_TOP() - MENTRY_FRAME_SIZE; // the x0's save slot
\*p_fcsr = 0;
#endif
write\_csr(mepc, fn); // mepc = kernel\_start
register uintptr_t a0 asm ("a0") = arg0; // a0 = mhartid
register uintptr_t a1 asm ("a1") = arg1; // a1 = dtb
asm volatile ("mret" : : "r" (a0), "r" (a1)); // 执行mret指令
\_\_builtin\_unreachable(); // 通知编译器这一行,在CPU运行时永远不会到达
}
这些准备操作,包括配置mstatus、mscratch、mepc寄存器,执行mret指令等。
此外,其中最重要的(这里暂时不关心中断),有3步(与退出异常过程类似):
- mstatus.MPP = PRV_S
设置为异常发生前为S模式;以便在异常结束后,能够使用MPP值,恢复出异常发生之前的工作模式S。 - mepc = kernel_start
将kernel_start写入mepc寄存器;以便执行mret指令时,可以从mepc中取出地址,赋给pc,实现跳转到kernel。 - 执行mret指令
此外,linux kernel启动时,还要求有2个参数:
- a0寄存器,用于保存mhartid;
- a1寄存器,用于保存设备树的物理内存地址。
因此,我们需要将mhartid和dtb作为参数,传递给kernel。
完成上述准备工作后,bbl64.bin代码,就执行完毕了。
下一节,我们看看TinyEMU,在执行mret指令时,会进行什么处理。
3 tinyemu处理mret指令
tinyemu中,在glue函数mret分支下,处理mret指令,如下:
static void no_inline glue(riscv_cpu_interp_x, XLEN)(RISCVCPUState \*s,
int n_cycles1
{
...
case 0x302: /\* mret \*/
{
if (insn & 0x000fff80)
goto illegal_insn;
if (s->priv < PRV_M)
goto illegal_insn;
s->pc = GET\_PC();
handle\_mret(s);
goto done_interp;
}
break;
}
我们看下,handle_mret函数中,具体处理,如下:
static void handle\_mret(RISCVCPUState \*s)
{
int mpp, mpie;
mpp = (s->mstatus >> MSTATUS_MPP_SHIFT) & 3; // mpp = mstatus.MPP
/\* set the IE state to previous IE state \*/
mpie = (s->mstatus >> MSTATUS_MPIE_SHIFT) & 1;
s->mstatus = (s->mstatus & ~(1 << mpp)) |
(mpie << mpp);
/\* set MPIE to 1 \*/
s->mstatus |= MSTATUS_MPIE;
/\* set MPP to U \*/
s->mstatus &= ~MSTATUS_MPP;
set\_priv(s, mpp); // 设置为mpp模式
s->pc = s->mepc; // pc = mepc
}
主要有以下操作:
- 恢复mstatus寄存器;
- 设置工作模式为mstatus.MPP,这里就是S模式;
- pc = mepc,从mepc中读取值,赋值为pc,这里mepc为kernel_start。
在tinyemu处理mret指令后,就会切换到S模式,并且pc跳转到0x80200000处,开始执行kernel代码。
因此,在riscv架构中:
- Bootloader/固件,运行在M模式下;
- Linux Kernel,运行在S模式下;
- 应用程序,运行在U模式下。
4 kernel执行过程
本小节中涉及代码,来自linux源码。
kernel源码,也包括2部分,汇编文件与C文件,启动过程中,我们通常称为汇编阶段和C语言阶段。
4.1 确定kernel入口点
Linux内核的链接脚本文件为,linux源码目录下的arch/riscv/kernel/vmlinux.lds,通过链接脚本,可以找到Linux内核的第一行程序是从哪里执行的。
kernel的入口点,在arch/riscv/kernel/head.S文件中_start
位置处(head.S就是kernel的启动文件),如下:
因此,kernel的入口点,第一条指令为csrw sie,zero
,即该指令pc为0x80200000。
4.2 执行head.S
进入执行kernel代码后,其实涉及的内容,就是linux启动流程,相关的内容了。
由于本文重点,不在探讨linux系统上,因此不做赘述。
可参考以下文档:
5 riscv启动流程小结
上述启动流程,可用下图,来简要描述:
6 调试技巧
- 如何判断Bootloader,开始执行?
跟踪Bootloader的入口点PC(0x80000000),第一条Bootloader指令,从这里开始执行。
- 如何判断Bootloader,执行结束?
我们在调试过程中,可以捕捉第一次mret指令,以便跟踪bootloader的终点,以及kernel的起点(因为进入kernel时,一定是从M模式切换到S模式,必须调用mret指令)。
- 如何判断Kernel,开始执行?
跟踪Kernel的入口点PC(0x80200000),第一条Kernel指令,从这里开始执行。
- 如何判断TinyEMU中,正在调试执行的Bootloader/Kernel的机器码,是否与预期一致?
比如:
我们跟踪,Bootloader的入口点PC(0x80000000),此时TinyEMU中依次取到的每一条指令,预期应该是riscv-pk\machine\mentry.S文件中reset_vector处,顺序向下的一段指令。此时,我们可以,查看mentry.S目标文件mentry.o的反汇编信息,得到如下的机器码,此时便可用反汇编信息,与TinyEMU中捕获的机器码,进行一一对比,就能看出是否一致(大部分是一致的,小部分指令反汇编后,可能有差异)。
查看mentry.o的反汇编信息:riscv64-unknown-linux-gnu-objdump -d mentry.o > mentry.txt
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!