LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 1217|回复: 0

移植NetBSD到AMD x86-64:操作系统可移植性事例研究

[复制链接]
发表于 2008-5-24 17:53:46 | 显示全部楼层 |阅读模式
本文是这篇文章的翻译:http://www.usenix.org/events/bsd ... linden/linden_html/
Frank van der Linden
Wasabi Systems, Inc.
fvdl@wasabisystems.com
译者:不开花
bukaihua0001@gmail.com


摘要:
众所周知NetBSD是移植性非常好的操作系统, 当前运行在44个不同架构上(12种CPU类型)。本文探究使其可移植所采取的措施 ,和这是如何减少移植NetBSD到新架构所需工作量的。AMD x86-64架构规范公布于2000年,真实硬件出现于2002年,本文就以此为例。
1 可移植性
从项目伊始,支持多平台就是NetBSD的一个主要目标。随着NetBSD移植到越来越多的平台,NetBSD内核代码随着改造成可移植性更好。
1.1 概要
一般地,代码在各个移植间尽可能的共享。在NetBSD里,应当总是考虑到代码对于现在将来能否用于不同的架构。如果这样,它就是机器不相关的并把它放到源码树的合适目录里。当打算编写机器不相关的代码时,包含了依赖于架构的条件预处理语句,那么代码很可能就是错误的,或是需要一个额外的抽象层来移除这些语句。
1.2 类型
不对类型尺寸做任何假设。假设类型尺寸为32位平台上的当64位平台出现时就是个大问题。在1994年移植NetBSD到DEC Alpha时已经解决了大多数此类问题。在1998年移植到64位,大尾端的UltraSPARC (sparc64)(相对于Alpha的小尾端)时,解决了一些此类问题的变体。当和固定大小尺寸的数据结构交互时,例如磁盘上文件系统的元数据,或由硬件设备直接解释的数据结构,使用固定尺寸类型,例如uint32_t, int8_t等等。
1.3 设备驱动
BSD最初考虑是为一个目标平台(PDP11, 接着是VAX)编写。随后添加了为别的平台编写的代码, 4.4BSD包含了用于4个平台的代码。 NetBSD基于4.4BSD, 但是逐年增加了所支持的平台数。随着添加的平台数越来越多,很显然有很多平台使用相同的硬件设备,而使用了不同的底层方法访问设备寄存器和处理DMA。举个例子,这导致五个不同的移植对于串口使用五种不同的驱动,而包含几乎一样的代码。对于到新硬件的移植几个月才能添加进去,显然这种情况是不可接受的。
为了改变这种情况, 创建了bus_dma和bus_space层[1], [5]。bus_space层负责处理设备I/O空间访问, bus_dma层处理DMA访问。对于移植NetBSD到新的架构,机器所用的每个I/O总线都要实现这些接口。如果这么做,所有挂接到这样的I/O总线上的设备应能找到并轻易地工作。
2 机器相关部分
当然,不是所有的代码都可以共享的。一些部分处理特定平台的硬件, 或是需要使用编译器无法产生的机器指令。一些用户空间工具也是用于特定平台的。这里是当移植NetBSD到信平台时,NetBSD与机器相关的最重要的部分的概述。
2.1 工具链
首先并最重要的是有一个可工作的交叉工具链(编译器,汇编器,连接器,等等)来在新平台上自举操作系统。GNU工具链已经成为开源系统的事实标准, NetBSD也不例外。由于几乎所有的NetBSD移植都用ELF二进制格式(对于任何新的移植也要用它),同时像Linux等操作系统使GNU工具链除了需要创建/修改配置文件以外不需要更多的工作就可用于NetBSD。(GNU工具链)根本就不支持目标CPU这种情况例外,这需要更多的努力。
2.2 引导代码
引导代码负责加载内核映象到内存并执行。它和固件一起加载映象。这个工作需要编写很大程度上依赖于固件所提供函数的代码。通常由于固件容量的限制,其仅用于加载二级引导装入程序(bootloader), 包含更复杂的用于处理驻留在内核映象里的文件系统和内核文件格式的代码。
2.3 陷入和中断
陷入和中断处理显然是内核里高度机器相关的部分。就在最近,对于这些入口点必须有机器相关的代码来保存和恢复CPU寄存器。此外, 处理器有不同的陷阱集,需要特别注意。
2.4 底层VM / MMU处理
内存管理单元(MMU)对于不同CPU相差很大。对于一簇相同架构的处理器也可能是不同的(例如, PowerPC 4xx系列和6xx系列的MMU就很不同)。在硬件上也可能差异很大。例如MMU硬件可能有固定的页表结构,或是把很多操作留给软件, 表现为转换后援缓冲器(TLB) 缺失。底层虚存代码在所有4.4BSD派生系统里都称为pmap模块,这个名字是从Mach操作系统里拿来的,4.4BSD使用Mach操作系统的虚存(VM)系统。
2.5 特定于移植的设备
有些平台有和别的平台上很不一样的设备。可能包括片上串口或时钟。移植NetBSD到一个新平台通常需要为至少一个这样的设备编写驱动程序。
2.6 总线层后端代码
如上所叙,对于I/O和DMA操作,设备代码使用机器不相关的接口。不同的平台隐藏于这个接口之下,这个接口的实现处理特定平台的操作。这个接口频繁的用于设备驱动中,所以内存占用和速度是很重要的问题。通常这个接口由一簇宏或内联函数实现。
2.7 库
用户空间里, C启动代码和一些库会包含机器相关代码。库部分的问题主要是C库里的系统调用接口,相同库里的优化串函数,以及数学库里特定的浮点处理。还有一些别的,很少使用的区域,像处理读取内核内存的KVM库。最后,不同平台的共享库处理(重定位类型)应该是不同的。
3 x86-64硬件
在开始具体工作之前,首先给个AMD x86-64架构[2]的简介。AMD x86-64 (代号``Hammer'') 架构规范发布于2000年底。在2001年12月发布本文时,还没有公开可用的硬件实现(NetBSD/x86-64完全是在由VirtuTech制作的 Simics x86-64模拟器上开发的)。由于x86-64基本上是IA32 (或i386, 如同在NetBSD所称)架构的扩展, 下面首先介绍它的前身。

图1: IA32虚拟地址转换(4K的页)。页表和页目录的入口是32位宽。
3.1 IA32架构
IA32是Intel对首先由80386引入的应用于PC世界,今天已成为最流行的CPU架构的32位架构的称呼。
IA32 CPU有如下特征[4]:
7个32位通用寄存器,其中4个也可用于16位和8位组。
很大指令集。
从80486起,带有片上浮点单元。
从Pentium III起,带有流式SIMD扩展(SSE), 一类负责处理并行加载/存储和计算的针对图形应用的指令。SSE也增添了一些新的64位宽用于SSE指令的寄存器。Pentium 4添加了更多的SSE指令和128位宽的SSE寄存器。这些新的SSE指令称为SSE2。
32位寻址
MMU支持页保护方案,和通过树型页表支持4G虚存。虚地址的不同部分用于页表结构的指示。这个结构包含一些页的保护信息,并包含指向用于索引下层页的物理地址。页表的最低层包含要用到的实际物理地址。后来的CPU也支持这种方案的一个称为(PAE)的扩展版本,允许使用超过4G物理内存。用于IA32架构的虚地址转换如图1所示。
内存段通过描述符描述类型,基地址和内存段长度。内存描述符(和别的类型)保存在 相关的表里。
指定使用哪个描述符的段寄存器来决定加载/存储/执行的位置。
陷入/中断处理通过特定的描述符数组,称为中断描述符表(IDT)。
四种不同的程序执行模式: 纯16位模式 (``实模式'',向后兼容8086); 16位模式 ``保护模式'' (带保护的16位模式); 32位 ``保护模式'' (这是所有的现代系统所用的); 最后是 ``虚8086模式'',这用于在虚拟“实模式”下运行旧的16位程序,而操作系统实际运行在32位保护模式下。
3.2 x86-64通用扩展
x86-64架构大体上是IA32架构的64位扩展。除了传统的``实'' (16-bit)和 ``保护'' (32-bit)模式,它定义了一个 ``长'' (64-bit)模式。在实模式和保护模式,它完全兼容IA32架构。在长模式, 可以不做任何修改运行32位二进制程序,并包含一组扩展。本文仅讨论长模式,此已点明,别处不累述。
3.3 寄存器
TIA32架构现存的通用寄存器扩展到64位。添加了第八个通用寄存器, 总共为15(不包括esp寄存器,参见图2)。这源于对IA32架构的一个牢骚:通用寄存器太少。此外,添加了八个SSE2寄存器。为了一致,所有的通用寄存器在指令里都可以以低16位或低8位寻址,对于IA32架构只能使用四个寄存器。
为了向后兼容,低16位和低8位的计算和移动不影响高位。然而,32位操作是0扩展。添加了一个特殊的移动寄存器里64位立即数的指令以便于使用64位常值;在别的指令里不能出现64位立即数。

图 2: x86-64寄存器。 IA32兼容寄存器以斜体标示
3.4 内存管理
x86-64是IA32的扩展也题现在内存管理单元(MMU)里。 x86-64有64位虚存寻址空间, 然而,这个架构的最初实现知使用了64位中的48位, 和40位物理地址空间。地址有符号扩展指定, 导致在虚存空间产生了一个无法寻址的``洞''。为了让MMU来处理48位虚地址空间,添加了一个额外的页表层,此外,这个额外层添加到了以后的IA32 实现,以支持PAE (参见图 3, 在虚地址里加点的行表明PAE在哪终止)。大体上, x86-64页表方案是IA32PAE方案,使用512代替4页表指针目录入口,加上四级,称为PML4。如此的相似以至必须实际指明允许PAE特征作为使芯片进入长模式步骤的一部分。

图 3: x86-64里的虚地址转换 (4K页面)。页表结构的入口是64位宽。
3.5 其余
x86-64架构一些值得注意的别的特性包括:
相对IP寻址。
之解的地址空间( 忽略了通过代码段和数据段指定的内存偏移)。
大多数硬件(系统)数据结构扩展以使在需要时可处理64位寻址。
快速的, 专门包装的用于操作系统系统调用。 AMD已经和K6一起介绍了这些,也都扩展到64位。
添加了一些特定的寄存器,例如保存SYSCALL/SYSRET指令入口点的寄存器, 和EFER, 扩展特性允许寄存器, 也包含一些别的东西,包含允许长模式的位。
4 实际的移植
使用第2部分所列的机器相关操作系统, 这里讨论移植NetBSD到x86-64架构的工作。
4.1 工具链
当开始NetBSD/x86_64这项工作的时候,GNU工具链对X86_64已经有基本的支持了,是在SuSe上为Linux开发的。还不支持共享库, 但是编译,汇编和连接等应用已可工作。已经定义了应用程序二进制接口(ABI)[3]。为NetBSD修改这些代码只需要修改/创建一些配置头文件。工具链也存在一些操作系统不相关的编译器和链接器bug,但是这可看成x86_64代码还很年轻。
这里有一些要考虑的ABI问题。 x86-64 ABI定义了四个非位置独立代码(non-PIC)模式 :
小。程序里的所有符号假设为32位虚地址。
内核。同上, 但与之相反,所有的地址看成相反的32位范围, 例如,在64位内存的高32位。 内核通常运行在虚存的高位,添加这个代码模型来映射内核到那个区域,而不用在代码里指定64位偏移。
中等。认为代码段的大小和地址在32位范围内,但是数据可以是全64位
大。 对于数据和代码地址没限制。编译器必须产生来使用通过寄存器的非直接地址来确保可寻址64位范围。
对于位置相关代码也有类似模式。用户空间程序默认使用“小”代码模式。在做移植时其它的代码模式还不完全支持,然而,至少“大”模式在做少量修改后也可稳定用于内核。
4.2 引导代码和自举
由于还没有实际可用的x86-64硬件, 对于已经指定即将退出的x86-64机器也没有确定的固件接口, 引导代码只能处理模拟器所提供的了。模拟器提供了一个通常的PC BIOS 接口, 这意味着几乎可以原样使用NetBSD/i386的启动代码。然而,要加载的内核映象是用于x86_64的64位ELF二进制程序。这项工作并不需要太多修改,因为加载64位ELF二进制程序已经是机器不相关的了并放到各个平台上NetBSD引导代码所用的单独的库里了。
对于新的NetBSD移植,内核里的初始化启动代码(例如,内核里第一个被执行代码)自然要从头编写,尽管这可视为NetBSD/i386的扩展版本。由于x86-64在开机时是完全兼容IA32的,也要负责使CPU走出16位模式,进入32位模式,接着是一系列步骤是CPU进入64位模式。实际步骤是:
允许物理地址扩展
在EFER寄存器里设置LME (长模式允许)位
使%cr3寄存器指向一个预制初始化4级页表结构
允许分页
这时CPU运行在32位兼容段。 使用长模式内存段伪造一个临时的全局描述符表,并跳转到那个段
由于内核映射到内存的高层, 我们之前不能寻址那个区域,因为它位于32位范围之外。但是既然我们最终运行在长模式,它在跳转指令的范围内,因此使用一个来开始执行内核代码。
4.3 陷阱和中断
虽然没有代码可以共享,底层陷阱和中断代码的结构类似于NetBSD/i386的。x86-64也使用中断描述符表(IDT)来设置陷阱和中断向量。对于陷阱入口点的工作是通常的保存寄存器//调度/恢复寄存器。尽管高层陷阱代码可以在NetBSD/i386和NetBSD/x86-64间共享,这是由于陷阱集在架构间是相同的,但是还没有这么做。
另一个陷阱集是系统调用入口点。x86-64支持存在于IA32架构的相同机理: 通过软件中断进入内核,或是通过处理到特殊类型结构(调用门)的调用来这么做,这回自动选择到内核环境。这些指令执行一些通常不需要在固定地址空间(例如,全部4G虚存对于一块程序可用,内核通常占据上层区域)。SYSCALL和SYSRET指令优化了这种情况,可用于实现一个快速的系统调用路径,这可能成为应用中一个重要的性能因素。处理这个的代码已经写好,但还没集成进去;当前仍在使用旧风格的入口点,但在不久的将来会改变。
4.4 底层VM / MMU处理
x86-64 MMU 使用非常类似于IA32的页表结构。基本上是IA32带个物理地址扩展页表,外推到4级,来处理x86-64处理器家族的48位虚存。由于这种相似性,使用了i386的pmap模块, 并在无意间实现了一个通用的带有32或64位宽度的N级IA32页表。最终的代码在NetBSD的x86-64和i386移植上都做了测试, 也成功了。IA32和x86-64代码的不同隐藏在 C预处理宏和类型定义里。这种方法的好处是现在在NetBSD/i386上也易于选择支持PAE,因为这只意味着条件编译一些宏和类型定义。

图4: NetBSD/x86-64 虚存布局。
实现的虚存布局示于图4。由于CPU在虚地址上执行的符号宽展,在2^47和2^64-2^47间有个不可寻址的空白。这是没用的;类似的空白也存在于例如SPARCv9和Alpha处理器上。 内存布局或多或少是IA32内存映射的延伸, 最好能合并在一个pmap模块。运行在虚存下半部的用户进程,而内核总是映射到上半部。上半部的一部分没用到, 因为内核不需要大量可用虚存, 结果使用这么些空间以不合适的方式放大了一些数据结构。虚存下半部的上部用于递归映射的页表,虚存上半部的上部也是这样。
这种布局意味着内核已超ABI中规范的“内核”代码模式的范围。需要``大''模式, 但是gcc还不支持。幸运的是,在做了一些修改后,它最终可以工作。NetBSD/x86_64内核将可能更改来使用这个ABI模型,一旦真实硬件可用, 将会加速评估。现在使用这个布局是因为它是IA32模式的一致扩展,使pmap代码易于共享。
4.5 总线层后端代码
大多数总线层后端代码可重用i386的。对于PIO情况,指令是一样的,除了为了适应64位寄存器集所做的一些修改外,其余不需修改。内存映射I/O也是这样。DMA框架需要处理可能带有的32位PCI,这不能使用DMA访问内存4G以上的空间。现在,对于有用于32位PCI的DMA内存总是来自4G之内。这需要稍后重新访问; 对于有超过4G内存的机器情况可能在RAM可用的地方发生,但不是在4G限定内。为了避免这个问题, 可引入bounce buffers ; 它们是所做DMA上的临时缓冲,并到或从数据要复制到的实际位置。
4.6 特定移植的设备
至今, NetBSD x86-64移植没有处理任何平台相关的设备。模拟器模拟了PC世界里常见的硬件组件(例如主机PCI桥等)。在bus_space和bus_dma层实现后,不需要修改,这些组件 ``刚好可工作''。
4.7 库
在用户空间的主要工作是移植库和C启动代码。C启动代码和C库移植相当流畅。这项工作的大部分是写系统调用的存根和优化串函数,要意识到x86-64 ABI传递参数大多是在寄存器里,而不像i386ABI规范里总是在堆栈上。数学库需要稍多一点工作。可以和i386(实际是i387)共享很多代码,尽管FPU有相同的指令,但是有ABI的不同。x86-64 ABI规定浮点参数在SSE寄存器里传递,但是i386 ABI在堆栈上传递。要写一些宏来析取各种参数到各种(大多数是三角)函数, 准备并为i386和x86-64移植使用通用的代码。最后,修改动态连接器来处理x86-64共享库可能用到的重定位类型。
4.8 兼容代码
x86-64提供了不经修改运行32位i386应用程序的选项。这是个有用的选项, 因为它允许操作系统运行旧的应用程序。然而在内核需要有对此的支持, 基本要求是为了运行32位应用程序需要在CPU的各种描述符表里安装32位兼容内存段。CPU就会执行32位环境的段的指令。然而,陷入到内核会选择64 位模式。这样的好处是对于32位应用程序不需要特殊的内核入口和退出代码。 32位程序到内核的接口确实不同;它们传递给系统调用参数的方式不同。还有,传递到内核的结构(更确切的说, 到它们的指针)对齐是不同的。这个问题可追寻到允许运行旧的NetBSD/i386二进制程序。
用于从各种平台 (Linux, Tru64, Solaris等等)运行兼容代码成为NetBSD的一部分已很久了。因此,不要对此感到惊奇, 之前已经处理过这个问题,当NetBSD/sparc64需要运行32位SPARC二进制程序。这个代码, compat_netbsd32 模块, 实现了一个转换(如果需要)32位参数到系统调用给其64位相关者。
5 结论和未来的工作
移植NetBSD到AMD x86-64架构花了六周,证实了NetBSD移植性非常好的操作系统的评价。一周用于设置交叉工具链和阅读x86-64规范,三周用于编写内核代码,一周用于编写用户空间代码,还有一周测试和调试。在测试中没有发现内核任何机器不相关部分的问题;所有的(模拟的)设备,文件系统等等都没做过修改。
移植工作非常流畅。表 1 显示了新编写的代码量。


表 1: NetBSD源码树每个部分新编写的 C/汇编代码行数
区域
汇编行数
C行数

libc
310
2772

C 启动代码
0
104

libm
52
0

内核
3314
17392

动态连接器
59
172




在“相关”CPU区域间共享的代码,还可以总更多的工作。当前, x86-64 pmap不应在两个移植间共享,尽管对于两个架构都能工作。 pmap代码记成在表1里记成“新代码”,但是3500行代码里的大多数和i386的形式相同或基于i386的代码。一些描述表代码也可共享。当代码合适的共享时,新的C代码行数可降到10,000行以内。
鸣谢
这项工作由我的雇主Wasabi Systems, Inc出资。我还要感谢AMD的支持,及出自Virtutech的模拟器,还有在SuSe上制作Linux工具链的人们。
参考文献
1
Jason Thorpe: 用于NetBSD的机器无关的DMA框架, Usenix 1998年技术参考年刊。
2
超微设备公司: The AMD x86-64架构编程概述,
http://www.amd.com/products/cpg/64bit/pdf/x86-64_overview.pdf
3
Hubicka, Jaeger, Mitchell: x86-64 draft ABI, http://x86-64.org/abi.pdf
4
Intel公司: Pentium 4手册, http://developer.intel.com/design/Pentium4/manuals/
5
Chris Demetriou: NetBSD bus_space(9)手册页,出自NetBSD 1.3, 1997.
... x86-641
x86-64是超微设备公司(AMD)的商标。
Frank van der Linden
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表