CXD Linux Engineer

移植Linux到一个新的处理器架构上 part 1


前言

这篇文章是翻译这三篇文章的第一篇:
Porting Linux to a new processor architecture, part 1: The basics
Porting Linux to a new processor architecture, part 2: The early code
Porting Linux to a new processor architecture, part 3: To the finish line

正文

虽然一个简单的移植只需要4000行左右的代码,但是让Linux内核在一个新的处理器架构上运行是一个非常困难的事情。更糟糕的是没有多少有用的文章来描述移植过程。这一系列的三篇文章的目标是提供一个移植Linux内核的大致过程,至少当你移植Linux内核到一个新的处理器架构上时可以作为一个参考。

在花了无数的时间越来越顺畅的移植内核支持许多架构后我发现一个定义良好的框架在移植过程中可以大量参考。这样的框架在逻辑上被分为两个紧密相连的部分。第一部分是启动代码,架构特定的代码是在内核接管bootloader后执行直到init的最终执行。第二部分关注的是当启动阶段已经完成内核进入正常运行状态时经常执行的架构特定代码,第二部分包含开始运行新的线程、处理硬件中断或者软件异常、复制数据给用户程序或者从用户程序复制数据、处理系统调用等等。

有必要进行一个全新的移植吗?

就像LWN去年发布的另一篇有关于移植的文章,移植分三种不同的层次:

  1. 移植到一个新的开发版上,它的处理器已经被支持。
  2. 移植到一个新的处理器上,但是和他架构相同的处理器系列已经被支持。
  3. 移植到一个全新的处理器架构上。

此处有一部分没有翻译

了解你的硬件

真正的了解底层硬件是最重要的基础,它是移植Linux最主要的前提条件。

通常处理器在逻辑上或者物理上至少分为两部分。第一部分通常是用户模式下的ISA细节,这基本上意味着用户模式下处理器需要理解和执行的指令序列。第二部分描述了特权架构,他包括只能在内核模式下执行的指令序列和各个控制处理器状态的寄存器。

第二部分是移植主要关注的信息,这也是阻止开发者重用其他架构代码的主要原因。

我们需要知道的几个最主要的问题是:

  1. 该处理器架构的虚拟内存模型是什么,页表的格式和翻译机制?

    许多处理器架构(例如:x86,ARM)定义了一个灵活的虚拟内存布局,他们的虚拟地址空间理论上可以任意划分为用户空间和内核空间,但是在32位处理器上Linux的默认划分方式是3GB的低地址部分划分给用户空间剩下的1GB的高端地址给Linux内核空间。在其他架构上,内存布局非常受限于硬件设计。例如MIPS32它的虚拟地址被固定分为两段相同大小的空间:低2GB空间被指定为用户空间,高2GB用于内核空间。内核空间的物理地址甚至已经被预定义分为几个不同地址段。

    页表的格式和处理器使用的虚拟地址到物理地址的翻译机制有紧密联系。当使用硬件管理机制时,当TLB(一个硬件缓存用于存放最近使用的虚拟地址到物理地址的翻译)不包含一个给定的虚拟地址的翻译(称为TLB miss),硬件状态机会自动从内存中的页表结构中获取正确的翻译并将此翻译缓存到TLB中。这就意味着页表格式是固定的并且是由处理器规范定义的。当使用软件管理机制时,一个TLB miss异常是被一段代码处理的,理论上页表的格式是怎样组织的是非常自由的,只是TLB的格式是固定的。

  2. 怎样使能或者失能中断,特权模式和用户模式之间怎样切换,怎样捕获异常等?

    虽然所有的这些操作通常只涉及在一组寄存器上读或者修改某些比特位,但是他们总是架构特定的。正是由于这个原因,大多数情况下他们是由一段专用的汇编代码实现。

  3. 什么是ABI?

    虽然可能有人认为Application Binary Interface(ABI-应用程序二进制接口)的支持是编译工具链的事情,因为它定义了堆栈初始化为栈帧的方法,函数之间参数和返回值的传递方式等等。但是移植Linux内核这是完全有必要知道的。例如,作为系统调用的接受者(这通常是ABI定义的)内核需要知道在哪里得到参数,怎样返回值;或者在上下文切换时,内核必须知道哪些数据需要保存和恢复,以及线程的上下文是由什么构成的,等等。

了解内核

学习一点内核概念,特别是关于Linux内存布局的知识会有很大帮助。我承认我花了一段时间来弄清楚low memoryhigh memory,以及direct mapping(直接映射)和vmalloc regions(vmalloc区域)的区别。

一个普通的,简单的移植(在32位处理器上)内核占据虚拟地址的高端1GB地址空间,这是非常简单的。这1GB空间在Linux中定义为直接映射到物理内存的底端(称为low memory),这意味着如果内核访问地址0xC0000000,它会被重定向在物理地址的0x00000000处。

相反,在一个物理内存多余这个直接映射的区域的系统上,上面的内存区域(称为 high memory)内核不能正常访问。所以其他机制必须被使用,例如kmap()kmap_atomic(),用于访问这些高端内存页。

在直接映射区域之上是vmalloc区域他被vmalloc()函数控制。这种分配机制可以分配虚拟地址上的连续空间但是物理地址上是不连续的。这在要求分配大量连续的内存页但是在物理空间上没有这么多的连续空间是非常有益的。

阅读更多关于Linux内存管理的知识可以在Linux Device Drivers-PDF和这篇文章中找到。

怎样开始?

当你满脑子都是处理器规定和内核规则时,该为新创建的架构目录添加一些文件了。但是等等…我应该在哪,怎样开始?所有移植甚至是所有代码都必须遵守某些API。这要分为两个步骤。

首先,一些文件和定义一些符号(函数,变量,定义)对于内核甚至编译是非常必要的。这些文件和符号可以从编译失败的信息中推到出来:如果编译失败是因为缺少某些文件或者符号。这是一个很好的指示,你应该使用它(或者有时候一些配置选项需要改变)。在移植Linux时这种方法是非常有效的当需要实现大量的头文件来定义架构特定的代码和内核之间的API。

编译之后的内核是能够在目标硬件上运行的,我们需要知道启动代码是非常有顺序,他允许很多函数开始时为空函数然后逐渐被实现直到系统最后变得稳定并且运行init进程。在早期汇编启动代码执行后,运行C代码是一个常用的做法。然而early_printk()等一些基础函数建议尽早实现不然非常难以调试。

最后准备开始:最简易的一组无代码文件

移植编译工具到一个新的处理器架构上是移植Linux内核的前提条件,这里我们假设已经完成。就编译工具而言最后需要做的是建立交叉编译器。移植C标准库还没有完成,仅仅完成了交叉编译器的阶段一。

这样的交叉编译器只能编译裸机代码,但是非常适合编译内核因为内核不需要依赖任何外部库。相反,处于阶段二的交叉编译器是用来支持C标准库的。

移植Linux到一个新的处理器架构的第一个步骤是在内核源码树根目录下的arch/目录下新建一个目录(例如我建立的是linux/arch/tsar/),在这个新目录中它的文件布局是非常标准的:

  • configs/: 支持Linux系统的默认配置(即 *_defconfig 文件)
  • include/asm/: 仅供内部使用的头文件,即 Linux源文件
  • include/uapi/asm: 需要提供给用户空间的头文件(例如 libc库)
  • kernel/: 通用内核管理
  • lib/: 架构特定的优化程序(例如 memcpy(), memset())
  • mm/: 内存管理

一旦新的架构目录出现Linux自动知道它的存在。它只是抱怨没有找到新架构下的Makefile文件:

~/linux $ make ARCH=tsar
Makefile: ~/linux/arch/tsar/Makefile: No such file or directory

下面的例子是一个最简化的Makefile文件只有几个变量:

KBUILD_DEFCONFIG := tsar_defconfig

KBUILD_CFLAGS += -pipe -D__linux__ -G 0 -msoft-float
KBUILD_AFLAGS += $(KBUILD_CFLAGS)

head-y := arch/tsar/kernel/head.o

core-y += arch/tsar/kernel/
core-y += arch/tsar/mm/

LIBGCC := $(shell $(CC) $(KBUILD_CFLAGS) -print-libgcc-file-name)
libs-y += $(LIBGCC)
libs-y += arch/tsar/lib/

drivers-y += arch/tsar/drivers/
  • KBUILD_DEFCONFIG必须是一个有效的默认配置文件名,就是configs目录下的默认配置文件(configs/tsar_defconfig)
  • KBUILD_CFLAGSKBUILD_AFLAGS 定义编译选项,分别对应编译器和汇编器。
  • {head,core,libs,...}-y 列出的是被编译进内核映象的目标文件(或者是子目录名)详细请看Documentation/kbuild/makefiles.txt文件

arch/目录下的文件Kconfig有两个用途:架构特定的配置选项的帮助文档;选择架构无关的配置选项(即 那些在其他Linux源码中早已定义的选项)适用于这个架构。

由于它是新创建架构的主要配置文件,他的内容决定了menuconfig命令的布局(例如 make ARCH=tsar menuconfig)。在这个文件中怎加内容是非常困难的因为他非常依赖于特定架构,但是看这个文件中用于其他架构的选项是非常有帮助的。

defconfig文件(例如 configs/tsar_defconfig)对于Linux内核编译系统(kbuild)是有重要的。他的作用是定义这个架构的默认配置选项,它用于以这个基本配置为种子来配置生成一个全配置的Linux内核。可以参考其他架构下的defconfig文件。但是我们需要提炼他,因为对于支持例如:USBIOMMU甚至文件系统对于这个阶段来说太早了。

最后“不是真正的代码但是非常重要”的是创建一个脚本文件(通常放在kernel/vmlinux.lds.S)他会指导连接器怎样放置代码和数据的各个段在最后的内核映象中。例如,汇编启动代码必须放在二进制文件的最开始位置,是这个文件允许我们这样做。

总结

到达这一步后,编译系统已经可以使用了。现在可以生成一个初步的内核配置文件,定制它甚至可以通过它来编译内核。但是编译器会很快停止因为到目前为止还么有包含任何代码。

下一篇文章介绍移植的第二阶段,我会添加一些代码包括:头文件、早期汇编启动代码和所有重要的会被执行的函数直到第一个内核线程被创建。


Comments

Content