﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-Thinking in C++-文章分类-嵌入式开发</title><link>http://www.cppblog.com/yishanhante/category/8698.html</link><description /><language>zh-cn</language><lastBuildDate>Sat, 15 Nov 2008 06:05:52 GMT</lastBuildDate><pubDate>Sat, 15 Nov 2008 06:05:52 GMT</pubDate><ttl>60</ttl><item><title>经典推荐之嵌入式系统 Boot Loader 技术内幕(詹荣开)</title><link>http://www.cppblog.com/yishanhante/articles/66774.html</link><dc:creator>jay</dc:creator><author>jay</author><pubDate>Wed, 12 Nov 2008 16:09:00 GMT</pubDate><guid>http://www.cppblog.com/yishanhante/articles/66774.html</guid><wfw:comment>http://www.cppblog.com/yishanhante/comments/66774.html</wfw:comment><comments>http://www.cppblog.com/yishanhante/articles/66774.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/yishanhante/comments/commentRss/66774.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/yishanhante/services/trackbacks/66774.html</trackback:ping><description><![CDATA[<h2>&nbsp;</h2>
<div class=t_msgfont id=postmessage_449><font size=2>本文详细地介绍了基于嵌入式系统中的 OS 启动加载程序 ―― Boot Loader 的概念、软件设计的主要任务以及结构框架等内容。<br><br><strong>一、引言<br><br></strong>在专用的嵌入式板子运行 GNU/Linux 系统已经变得越来越流行。一个嵌入式 Linux 系统从软件的角度看通常可以分为四个层次： <br></font>
<ul>
    <li><font size=2>引导加载程序。包括固化在固件(firmware)中的 boot 代码(可选)，和 Boot Loader 两大部分。 </font>
    <li><font size=2>Linux 内核。特定于嵌入式板子的定制内核以及内核的启动参数。 </font>
    <li><font size=2>文件系统。包括根文件系统和建立于 Flash 内存设备之上文件系统。通常用 ram disk 来作为 root fs。 </font>
    <li><font size=2>用户应用程序。特定于用户的应用程序。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。常用的嵌入式 GUI 有：MicroWindows 和 MiniGUI 懂。 </font></li>
</ul>
<font size=2><br>引导加载程序是系统加电后运行的第一段软件代码。回忆一下 PC 的体系结构我们可以知道，PC 机中的引导加载程序由 BIOS(其本质就是一段固件程序)和位于硬盘 MBR 中的 OS Boot Loader（比如，LILO 和 GRUB 等）一起组成。BIOS 在完成硬件检测和资源分配后，将硬盘 MBR 中的 Boot Loader 读到系统的 RAM 中，然后将控制权交给 OS Boot Loader。Boot Loader 的主要运行任务就是将内核映象从硬盘上读到 RAM 中，然后跳转到内核的入口点去运行，也即开始启动操作系统。 <br><br>而在嵌入式系统中，通常并没有像 BIOS 那样的固件程序（注，有的嵌入式 CPU 也会内嵌一段短小的启动程序），因此整个系统的加载启动任务就完全由 Boot Loader 来完成。比如在一个基于 ARM7TDMI core 的嵌入式系统中，系统在上电或复位时通常都从地址 0x00000000 处开始执行，而在这个地址处安排的通常就是系统的 Boot Loader 程序。 <br><br>本文将从 Boot Loader 的概念、Boot Loader 的主要任务、Boot Loader 的框架结构以及 Boot Loader 的安装等四个方面来讨论嵌入式系统的 Boot Loader。 <br><br>二</font><font size=2><strong>、</strong></font><font size=2><strong>Boot Loader 的概念<br><br></strong>简单地说，Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序，我们可以初始化硬件设备、建立内存空间的映射图，从而将系统的软硬件环境带到一个合适的状态，以便为最终调用操作系统内核准备好正确的环境。 <br><br>通常，Boot Loader 是严重地依赖于硬件而实现的，特别是在嵌入式世界。因此，在嵌入式世界里建立一个通用的 Boot Loader 几乎是不可能的。尽管如此，我们仍然可以对 Boot Loader 归纳出一些通用的概念来，以指导用户特定的 Boot Loader 设计与实现。 <br><br><strong>1. Boot Loader 所支持的 CPU 和嵌入式板<br><br></strong>每种不同的 CPU 体系结构都有不同的 Boot Loader。有些 Boot Loader 也支持多种体系结构的 CPU，比如 U-Boot 就同时支持 ARM 体系结构和MIPS 体系结构。除了依赖于 CPU 的体系结构外，Boot Loader 实际上也依赖于具体的嵌入式板级设备的配置。这也就是说，对于两块不同的嵌入式板而言，即使它们是基于同一种 CPU 而构建的，要想让运行在一块板子上的 Boot Loader 程序也能运行在另一块板子上，通常也都需要修改 Boot Loader 的源程序。 <br><br><strong>2. Boot Loader 的安装媒介（Installation Medium）<br><br></strong>系统加电或复位后，所有的 CPU 通常都从某个由 CPU 制造商预先安排的地址上取指令。比如，基于 ARM7TDMI core 的 CPU 在复位时通常都从地址 0x00000000 取它的第一条指令。而基于 CPU 构建的嵌入式系统通常都有某种类型的固态存储设备(比如：ROM、EEPROM 或 FLASH 等)被映射到这个预先安排的地址上。因此在系统加电后，CPU 将首先执行 Boot Loader 程序。 <br><strong>下图1</strong>就是一个同时装有 Boot Loader、内核的启动参数、内核映像和根文件系统映像的固态存储设备的典型空间分配结构图。 <br><br><strong>图1 固态存储设备的典型空间分配结构</strong><br></font>
<p align=center><font size=2><a href="http://linux.chinaunix.net/mirror/www-128.ibm.com/developerworks/cn/linux/l-btloader/images/image001.gif" target=_blank><u><font color=#0000ff>http://linux.chinaunix.net/mirror/www-128.ibm.com/developerworks/cn/linux/l-btloader/images/image001.gif</font></u></a> <br></font></p>
<br><font size=2><br><strong>3. 用来控制 Boot Loader 的设备或机制<br><br></strong>主机和目标机之间一般通过串口建立连接，Boot Loader 软件在执行时通常会通过串口来进行 I/O，比如：输出打印信息到串口，从串口读取用户控制字符等。 <br><br><strong>4. Boot Loader 的启动过程是单阶段（Single Stage）还是多阶段（Multi-Stage）<br><br></strong>通常多阶段的 Boot Loader 能提供更为复杂的功能，以及更好的可移植性。从固态存储设备上启动的 Boot Loader 大多都是 2 阶段的启动过程，也即启动过程可以分为 stage 1 和 stage 2 两部分。而至于在 stage 1 和 stage 2 具体完成哪些任务将在下面讨论。 <br><br><strong>5. Boot Loader 的操作模式 (Operation Mode)<br><br></strong>大多数 Boot Loader 都包含两种不同的操作模式："启动加载"模式和"下载"模式，这种区别仅对于开发人员才有意义。但从最终用户的角度看，Boot Loader 的作用就是用来加载操作系统，而并不存在所谓的启动加载模式与下载工作模式的区别。 <br><br><strong>启动加载（Boot loading）模式：</strong>这种模式也称为"自主" （Autonomous）模式。也即 Boot Loader 从目标机上的某个固态存储设备上将操作系统加载到 RAM 中运行，整个过程并没有用户的介入。这种模式是 Boot Loader 的正常工作模式，因此在嵌入式产品发布的时侯，Boot Loader 显然必须工作在这种模式下。 <br><br><strong>下载（Downloading）模式：</strong>在这种模式下，目标机上的 Boot Loader 将通过串口连接或网络连接等通信手段从主机（Host）下载文件，比如：下载内核映像和根文件系统映像等。从主机下载的文件通常首先被 Boot Loader 保存到目标机的 RAM 中，然后再被 Boot Loader 写到目标机上的FLASH 类固态存储设备中。Boot Loader 的这种模式通常在第一次安装内核与根文件系统时被使用；此外，以后的系统更新也会使用 Boot Loader 的这种工作模式。工作于这种模式下的 Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口。 <br><br>像 Blob 或 U-Boot 等这样功能强大的 Boot Loader 通常同时支持这两种工作模式，而且允许用户在这两种工作模式之间进行切换。比如，Blob 在启动时处于正常的启动加载模式，但是它会延时 10 秒等待终端用户按下任意键而将 blob 切换到下载模式。如果在 10 秒内没有用户按键，则 blob 继续启动 Linux 内核。 <br><br><strong>6. BootLoader 与主机之间进行文件传输所用的通信设备及协议<br><br></strong>最常见的情况就是，目标机上的 Boot Loader 通过串口与主机之间进行文件传输，传输协议通常是 xmodem／ymodem／zmodem 协议中的一种。但是，串口传输的速度是有限的，因此通过以太网连接并借助 TFTP 协议来下载文件是个更好的选择。 <br>此外，在论及这个话题时，主机方所用的软件也要考虑。比如，在通过以太网连接和 TFTP 协议来下载文件时，主机方必须有一个软件用来的提供 TFTP 服务。 <br><br>在讨论了 BootLoader 的上述概念后，下面我们来具体看看 BootLoader 的应该完成哪些任务。 <br><br><strong>三、Boot Loader 的主要任务与典型结构框架<br><br></strong>在继续本节的讨论之前，首先我们做一个假定，那就是：假定内核映像与根文件系统映像都被加载到 RAM 中运行。之所以提出这样一个假设前提是因为，在嵌入式系统中内核映像与根文件系统映像也可以直接在 ROM 或 Flash 这样的固态存储设备中直接运行。但这种做法无疑是以运行速度的牺牲为代价的。 <br><br>从操作系统的角度看，Boot Loader 的总目标就是正确地调用内核来执行。 <br><br>另外，由于 Boot Loader 的实现依赖于 CPU 的体系结构，因此大多数 Boot Loader 都分为 stage1 和 stage2 两大部分。依赖于 CPU 体系结构的代码，比如设备初始化代码等，通常都放在 stage1 中，而且通常都用汇编语言来实现，以达到短小精悍的目的。而 stage2 则通常用C语言来实现，这样可以实现给复杂的功能，而且代码会具有更好的可读性和可移植性。 <br><br>Boot Loader 的 stage1 通常包括以下步骤(以执行的先后顺序)： <br></font>
<ul>
    <li><font size=2>硬件设备初始化。</font>
    <li><font size=2>为加载 Boot Loader 的 stage2 准备 RAM 空间。</font>
    <li><font size=2>拷贝 Boot Loader 的 stage2 到 RAM 空间中。</font>
    <li><font size=2>设置好堆栈。</font>
    <li><font size=2>跳转到 stage2 的 C 入口点。</font> </li>
</ul>
<font size=2><br>Boot Loader 的 stage2 通常包括以下步骤(以执行的先后顺序)： <br></font>
<ul>
    <li><font size=2>初始化本阶段要使用到的硬件设备。</font>
    <li><font size=2>检测系统内存映射(memory map)。</font>
    <li><font size=2>将 kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中。</font>
    <li><font size=2>为内核设置启动参数。</font>
    <li><font size=2>调用内核。</font>
    <li><font size=2><br></font></li>
</ul>
<font size=2><strong>3.1 Boot Loader 的 stage1<br><br></strong><strong>3.1.1 基本的硬件初始化</strong> <br><br>这是 Boot Loader 一开始就执行的操作，其目的是为 stage2 的执行以及随后的 kernel 的执行准备好一些基本的硬件环境。它通常包括以下步骤（以执行的先后顺序）： <br><br></font>
<ul>
    <li><font size=2>屏蔽所有的中断。为中断提供服务通常是 OS 设备驱动程序的责任，因此在 Boot Loader 的执行全过程中可以不必响应任何中断。中断屏蔽可以通过写 CPU 的中断屏蔽寄存器或状态寄存器（比如 ARM 的 CPSR 寄存器）来完成。 </font></li>
</ul>
<ul>
    <li><font size=2>设置 CPU 的速度和时钟频率。 </font>
    <li><font size=2>RAM 初始化。包括正确地设置系统的内存控制器的功能寄存器以及各内存库控制寄存器等。 </font>
    <li><font size=2>初始化 LED。典型地，通过 GPIO 来驱动 LED，其目的是表明系统的状态是 OK 还是 Error。如果板子上没有 LED，那么也可以通过初始化 UART 向串口打印 Boot Loader 的 Logo 字符信息来完成这一点。 </font></li>
</ul>
<ul>
    <li><font size=2>关闭 CPU 内部指令／数据 cache。 </font></li>
</ul>
<font size=2><br><strong>3.1.2 为加载 stage2 准备 RAM 空间</strong> <br><br>为了获得更快的执行速度，通常把 stage2 加载到 RAM 空间中来执行，因此必须为加载 Boot Loader 的 stage2 准备好一段可用的 RAM 空间范围。 <br><br>由于 stage2 通常是 C 语言执行代码，因此在考虑空间大小时，除了 stage2 可执行映象的大小外，还必须把堆栈空间也考虑进来。此外，空间大小最好是 memory page 大小(通常是 4KB)的倍数。一般而言，1M 的 RAM 空间已经足够了。具体的地址范围可以任意安排，比如 blob 就将它的 stage2 可执行映像安排到从系统 RAM 起始地址 0xc0200000 开始的 1M 空间内执行。但是，将 stage2 安排到整个 RAM 空间的最顶 1MB(也即(RamEnd-1MB) - RamEnd)是一种值得推荐的方法。 <br><br>为了后面的叙述方便，这里把所安排的 RAM 空间范围的大小记为：stage2_size(字节)，把起始地址和终止地址分别记为：stage2_start 和 stage2_end(这两个地址均以 4 字节边界对齐)。因此： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2>stage2_end＝stage2_start＋stage2_size</font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>另外，还必须确保所安排的地址范围的的确确是可读写的 RAM 空间，因此，必须对你所安排的地址范围进行测试。具体的测试方法可以采用类似于 blob 的方法，也即：以 memory page 为被测试单位，测试每个 memory page 开始的两个字是否是可读写的。为了后面叙述的方便，我们记这个检测算法为：test_mempage，其具体步骤如下： <br><br>1． 先保存 memory page 一开始两个字的内容。 <br>2． 向这两个字中写入任意的数字。比如：向第一个字写入 0x55，第 2 个字写入 0xaa。 <br>3． 然后，立即将这两个字的内容读回。显然，我们读到的内容应该分别是 0x55 和 0xaa。如果不是，则说明这个 memory page 所占据的地址范围不是一段有效的 RAM 空间。 <br>4． 再向这两个字中写入任意的数字。比如：向第一个字写入 0xaa，第 2 个字中写入 0x55。 <br>5． 然后，立即将这两个字的内容立即读回。显然，我们读到的内容应该分别是 0xaa 和 0x55。如果不是，则说明这个 memory page 所占据的地址范围不是一段有效的 RAM 空间。 <br>6． 恢复这两个字的原始内容。测试完毕。 <br><br>为了得到一段干净的 RAM 空间范围，我们也可以将所安排的 RAM 空间范围进行清零操作。 <br><br><strong>3.1.3 拷贝 stage2 到 RAM 中</strong> <br><br>拷贝时要确定两点：(1) stage2 的可执行映象在固态存储设备的存放起始地址和终止地址；(2) RAM 空间的起始地址。 <br><br><strong>3.1.4 设置堆栈指针 sp</strong> <br><br>堆栈指针的设置是为了执行 C 语言代码作好准备。通常我们可以把 sp 的值设置为(stage2_end-4)，也即在 3.1.2 节所安排的那个 1MB 的 RAM 空间的最顶端(堆栈向下生长)。 <br><br>此外，在设置堆栈指针 sp 之前，也可以关闭 led 灯，以提示用户我们准备跳转到 stage2。 <br>经过上述这些执行步骤后，系统的物理内存布局应该如下图2所示。 <br><br><strong>3.1.5 跳转到 stage2 的 C 入口点</strong> <br><br>在上述一切都就绪后，就可以跳转到 Boot Loader 的 stage2 去执行了。比如，在 ARM 系统中，这可以通过修改 PC 寄存器为合适的地址来实现。 <br><br><strong>图2 bootloader 的 stage2 可执行映象刚被拷贝到 RAM 空间时的系统内存布局</strong><br></font>
<p align=center><font size=2><a href="http://linux.chinaunix.net/mirror/www-128.ibm.com/developerworks/cn/linux/l-btloader/images/image002.gif" target=_blank><u><font color=#0000ff>http://linux.chinaunix.net/mirror/www-128.ibm.com/developerworks/cn/linux/l-btloader/images/image002.gif</font></u></a> <br></font></p>
<br><font size=2><br><strong>3.2 Boot Loader 的 stage2</strong> <br><br>正如前面所说，stage2 的代码通常用 C 语言来实现，以便于实现更复杂的功能和取得更好的代码可读性和可移植性。但是与普通 C 语言应用程序不同的是，在编译和链接 boot loader 这样的程序时，我们不能使用 glibc 库中的任何支持函数。其原因是显而易见的。这就给我们带来一个问题，那就是从那里跳转进 main() 函数呢？直接把 main() 函数的起始地址作为整个 stage2 执行映像的入口点或许是最直接的想法。但是这样做有两个缺点：1)无法通过main() 函数传递函数参数；2)无法处理 main() 函数返回的情况。一种更为巧妙的方法是利用 trampoline(弹簧床)的概念。也即，用汇编语言写一段trampoline 小程序，并将这段 trampoline 小程序来作为 stage2 可执行映象的执行入口点。然后我们可以在 trampoline 汇编小程序中用 CPU 跳转指令跳入 main() 函数中去执行；而当 main() 函数返回时，CPU 执行路径显然再次回到我们的 trampoline 程序。简而言之，这种方法的思想就是：用这段 trampoline 小程序来作为 main() 函数的外部包裹(external wrapper)。 <br><br>下面给出一个简单的 trampoline 程序示例(来自blob)： <br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br>.text<br><br>.globl _trampoline<br>_trampoline:<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;bl&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;main<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;/* if main ever returns we just call it again */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;b&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;_trampoline<br><br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>可以看出，当 main() 函数返回后，我们又用一条跳转指令重新执行 trampoline 程序――当然也就重新执行 main() 函数，这也就是 trampoline(弹簧床)一词的意思所在。 <br><br><strong>3.2.1初始化本阶段要使用到的硬件设备</strong> <br><br>这通常包括：（1）初始化至少一个串口，以便和终端用户进行 I/O 输出信息；（2）初始化计时器等。 <br>在初始化这些设备之前，也可以重新把 LED 灯点亮，以表明我们已经进入 main() 函数执行。 <br>设备初始化完成后，可以输出一些打印信息，程序名字字符串、版本号等。 <br><br><strong>3.2.2 检测系统的内存映射（memory map）</strong> <br><br>所谓内存映射就是指在整个 4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的 RAM 单元。比如，在 SA-1100 CPU 中，从 0xC000,0000 开始的 512M 地址空间被用作系统的 RAM 地址空间，而在 Samsung S3C44B0X CPU 中，从 0x0c00,0000 到 0x1000,0000 之间的 64M 地址空间被用作系统的 RAM 地址空间。虽然 CPU 通常预留出一大段足够的地址空间给系统 RAM，但是在搭建具体的嵌入式系统时却不一定会实现 CPU 预留的全部 RAM 地址空间。也就是说，具体的嵌入式系统往往只把 CPU 预留的全部 RAM 地址空间中的一部分映射到 RAM 单元上，而让剩下的那部分预留 RAM 地址空间处于未使用状态。 <strong>由于上述这个事实，因此 Boot Loader 的 stage2 必须在它想干点什么 (比如，将存储在 flash 上的内核映像读到 RAM 空间中) 之前检测整个系统的内存映射情况，也即它必须知道 CPU 预留的全部 RAM 地址空间中的哪些被真正映射到 RAM 地址单元，哪些是处于 "unused" 状态的。</strong> <br><br><strong>(1) 内存映射的描述</strong> <br><br>可以用如下数据结构来描述 RAM 地址空间中的一段连续(continuous)的地址范围：<br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br>typedef struct memory_area_struct {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;u32 start; /* the base address of the memory region */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;u32 size; /* the byte number of the memory region */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;int used;<br>} memory_area_t;<br><br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>这段 RAM 地址空间中的连续地址范围可以处于两种状态之一：(1)used=1，则说明这段连续的地址范围已被实现，也即真正地被映射到 RAM 单元上。(2)used=0，则说明这段连续的地址范围并未被系统所实现，而是处于未使用状态。 <br>基于上述 memory_area_t 数据结构，整个 CPU 预留的 RAM 地址空间可以用一个 memory_area_t 类型的数组来表示，如下所示： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br>memory_area_t memory_map[NUM_MEM_AREAS] = {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;[0 ... (NUM_MEM_AREAS - 1)] = {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; .start = 0,<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; .size = 0,<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; .used = 0<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;},<br>};<br><br><br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br><strong>(2) 内存映射的检测</strong> <br><br>下面我们给出一个可用来检测整个 RAM 地址空间内存映射情况的简单而有效的算法：<br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br><br>/* 数组初始化 */<br>for(i = 0; i &lt; NUM_MEM_AREAS; i++)<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;memory_map<em>.used = 0;<br><br>/* first write a 0 to all memory locations */<br>for(addr = MEM_START; addr &lt; MEM_END; addr += PAGE_SIZE)<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;* (u32 *)addr = 0;<br><br>for(i = 0, addr = MEM_START; addr &lt; MEM_END; addr += PAGE_SIZE) {<br>&nbsp; &nbsp;&nbsp;&nbsp;/*<br>&nbsp; &nbsp;&nbsp; &nbsp;* 检测从基地址 MEM_START+i*PAGE_SIZE 开始,大小为<br>* PAGE_SIZE 的地址空间是否是有效的RAM地址空间。<br>&nbsp; &nbsp;&nbsp; &nbsp;*/<br>&nbsp; &nbsp;&nbsp;&nbsp;调用3.1.2节中的算法test_mempage()；<br>&nbsp; &nbsp;&nbsp;&nbsp;if ( current memory page isnot a valid ram page) {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; /* no RAM here */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; if(memory_map<em>.used )<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;i++;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; continue;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;/*<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;* 当前页已经是一个被映射到 RAM 的有效地址范围<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;* 但是还要看看当前页是否只是 4GB 地址空间中某个地址页的别名？<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;*/<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if(* (u32 *)addr != 0) { /* alias? */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; /* 这个内存页是 4GB 地址空间中某个地址页的别名 */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; if ( memory_map<em>.used )<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;i++;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; continue;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;/*<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;* 当前页已经是一个被映射到 RAM 的有效地址范围<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;* 而且它也不是 4GB 地址空间中某个地址页的别名。<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;*/<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if (memory_map<em>.used == 0) {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; memory_map<em>.start = addr;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; memory_map<em>.size = PAGE_SIZE;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; memory_map<em>.used = 1;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;} else {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; memory_map<em>.size += PAGE_SIZE;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}<br>} /* end of for (&#8230;) */<br><br><br><br><br></em></em></em></em></em></em></em></em></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>在用上述算法检测完系统的内存映射情况后，Boot Loader 也可以将内存映射的详细信息打印到串口。<br><br><strong>3.2.3 加载内核映像和根文件系统映像</strong> <br><br><strong>(1) 规划内存占用的布局</strong> <br><br>这里包括两个方面：(1)内核映像所占用的内存范围；（2）根文件系统所占用的内存范围。在规划内存占用的布局时，主要考虑基地址和映像的大小两个方面。 <br><br>对于内核映像，一般将其拷贝到从(MEM_START＋0x8000) 这个基地址开始的大约1MB大小的内存范围内(嵌入式 Linux 的内核一般都不操过 1MB)。为什么要把从 MEM_START 到 MEM_START＋0x8000 这段 32KB 大小的内存空出来呢？这是因为 Linux 内核要在这段内存中放置一些全局数据结构，如：启动参数和内核页表等信息。 <br><br>而对于根文件系统映像，则一般将其拷贝到 MEM_START+0x0010,0000 开始的地方。如果用 Ramdisk 作为根文件系统映像，则其解压后的大小一般是1MB。 <br><br><strong>（2）从 Flash 上拷贝</strong> <br><br>由于像 ARM 这样的嵌入式 CPU 通常都是在统一的内存地址空间中寻址 Flash 等固态存储设备的，因此从 Flash 上读取数据与从 RAM 单元中读取数据并没有什么不同。用一个简单的循环就可以完成从 Flash 设备上拷贝映像的工作： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2>while(count) {<br>*dest++ = *src++; /* they are all aligned with word boundary */<br>count -= 4; /* byte number */<br>};<br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br><strong>3.2.4 设置内核的启动参数</strong> <br><br>应该说，在将内核映像和根文件系统映像拷贝到 RAM 空间中后，就可以准备启动 Linux 内核了。但是在调用内核之前，应该作一步准备工作，即：设置 Linux 内核的启动参数。 <br><br>Linux 2.4.x 以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。启动参数标记列表以标记 ATAG_CORE 开始，以标记 ATAG_NONE 结束。每个标记由标识被传递参数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和 tag_header 定义在 Linux 内核源码的include/asm/setup.h 头文件中： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br>/* The list ends with an ATAG_NONE node. */<br>#define ATAG_NONE&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;0x00000000<br><br>struct tag_header {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;u32 size; /* 注意，这里size是字数为单位的 */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;u32 tag;<br>};<br>&#8230;&#8230;<br>struct tag {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;struct tag_header hdr;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;union {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_core&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; core;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_mem32&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;mem;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_videotext&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;videotext;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_ramdisk&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;ramdisk;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_initrd&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;initrd;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_serialnr&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;serialnr;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_revision&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;revision;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_videolfb&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;videolfb;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_cmdline&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;cmdline;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; /*<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;* Acorn specific<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;*/<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_acorn&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;acorn;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; /*<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;* DC21285 specific<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;*/<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; struct tag_memclk&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;memclk;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;} u;<br>};<br><br><br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>在嵌入式 Linux 系统中，通常需要由 Boot Loader 设置的常见启动参数有：ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。 <br><br>比如，设置 ATAG_CORE 的代码如下： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br><br>params = (struct tag *)BOOT_PARAMS;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;hdr.tag = ATAG_CORE;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;hdr.size = tag_size(tag_core);<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;u.core.flags = 0;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;u.core.pagesize = 0;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;u.core.rootdev = 0;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params = tag_next(params);<br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>其中，BOOT_PARAMS 表示内核启动参数在内存中的起始基地址，指针 params 是一个 struct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数，计算紧临当前标记的下一个标记的起始地址。注意，内核的根文件系统所在的设备ID就是在这里设置的。 <br><br>下面是设置内存映射情况的示例代码： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br><br>for(i = 0; i &lt; NUM_MEM_AREAS; i++) {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; if(memory_map<em>.used) {<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;params-&gt;hdr.tag = ATAG_MEM;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;params-&gt;hdr.size = tag_size(tag_mem32);<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;params-&gt;u.mem.start = memory_map<em>.start;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;params-&gt;u.mem.size = memory_map<em>.size;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;params = tag_next(params);<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; }<br>}<br><br><br><br></em></em></em></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>可以看出，在 memory_map［］数组中，每一个有效的内存段都对应一个 ATAG_MEM 参数标记。 <br>Linux 内核在启动时可以以命令行参数的形式来接收信息，利用这一点我们可以向内核提供那些内核不能自己检测的硬件参数信息，或者重载(override)内核自己检测到的信息。比如，我们用这样一个命令行参数字符串"console=ttyS0,115200n8"来通知内核以 ttyS0 作为控制台，且串口采用 "115200bps、无奇偶校验、8位数据位"这样的设置。下面是一段设置调用内核命令行参数字符串的示例代码： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br>char *p;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;/* eat leading white space */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;for(p = commandline; *p == ' '; p++)<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; ;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;/* skip non-existent command lines so the kernel will still<br>&nbsp; &nbsp; * use its default command line.<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;*/<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if(*p == '\0')<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp; return;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;hdr.tag = ATAG_CMDLINE;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) &gt;&gt; 2;<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;strcpy(params-&gt;u.cmdline.cmdline, p);<br><br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params = tag_next(params);<br><br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>请注意在上述代码中，设置 tag_header 的大小时，必须包括字符串的终止符'\0'，此外还要将字节数向上圆整4个字节，因为 tag_header 结构中的size 成员表示的是字数。 <br><br>下面是设置 ATAG_INITRD 的示例代码，它告诉内核在 RAM 中的什么地方可以找到 initrd 映象(压缩格式)以及它的大小： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2>&nbsp;&nbsp;params-&gt;hdr.tag = ATAG_INITRD2;<br>params-&gt;hdr.size = tag_size(tag_initrd);<br><br>params-&gt;u.initrd.start = RAMDISK_RAM_BASE;<br>params-&gt;u.initrd.size = INITRD_LEN;<br><br>params = tag_next(params);<br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>下面是设置 ATAG_RAMDISK 的示例代码，它告诉内核解压后的 Ramdisk 有多大（单位是KB）： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br>params-&gt;hdr.tag = ATAG_RAMDISK;<br>params-&gt;hdr.size = tag_size(tag_ramdisk);<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br>params-&gt;u.ramdisk.start = 0;<br>params-&gt;u.ramdisk.size = RAMDISK_SIZE; /* 请注意，单位是KB */<br>params-&gt;u.ramdisk.flags = 1; /* automatically load ramdisk */<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br>params = tag_next(params);<br><br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>最后，设置 ATAG_NONE 标记，结束整个启动参数列表： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br>static void setup_end_tag(void)<br>{<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;hdr.tag = ATAG_NONE;<br>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;params-&gt;hdr.size = 0;<br>}<br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br><strong>3.2.5 调用内核</strong> <br><br>Boot Loader 调用 Linux 内核的方法是直接跳转到内核的第一条指令处，也即直接跳转到 MEM_START＋0x8000 地址处。在跳转时，下列条件要满足： <br><br>1． CPU 寄存器的设置： <br></font>
<ul>
    <li><font size=2>R0＝0；</font>
    <li><font size=2>R1＝机器类型 ID；关于 Machine Type Number，可以参见 <strong>linux/arch/arm/tools/mach-types。</strong></font>
    <li><font size=2>R2＝启动参数标记列表在 RAM 中起始基地址；</font> </li>
</ul>
<font size=2><br>2． CPU 模式： <br></font>
<ul>
    <li><font size=2>必须禁止中断（IRQs和FIQs）；</font>
    <li><font size=2>CPU 必须 SVC 模式；</font> </li>
</ul>
<font size=2><br>3． Cache 和 MMU 的设置：<br></font>
<ul>
    <li><font size=2>MMU 必须关闭；</font>
    <li><font size=2>指令 Cache 可以打开也可以关闭；</font>
    <li><font size=2>数据 Cache 必须关闭；</font>
    <li><font size=2><br></font></li>
</ul>
<font size=2>如果用 C 语言，可以像下列示例代码这样来调用内核： <br><br></font>
<table class=t_table style="WIDTH: 98%" cellSpacing=0 bgColor=#dadacf>
    <tbody>
        <tr>
            <td><font size=2><br><br>void (*theKernel)(int zero, int arch, u32 params_addr) = (void (*)(int, int, u32))KERNEL_RAM_BASE;<br>&#8230;&#8230;<br>theKernel(0, ARCH_NUMBER, (u32) kernel_params_start);<br><br><br><br></font></td>
        </tr>
    </tbody>
</table>
<font size=2><br>注意，theKernel()函数调用应该永远不返回的。如果这个调用返回，则说明出错。 <br><br><strong>四、关于串口终端<br><br></strong>在 boot loader 程序的设计与实现中，没有什么能够比从串口终端正确地收到打印信息能更令人激动了。此外，向串口终端打印信息也是一个非常重要而又有效的调试手段。但是，我们经常会碰到串口终端显示乱码或根本没有显示的问题。造成这个问题主要有两种原因：(1) boot loader 对串口的初始化设置不正确。(2) 运行在 host 端的终端仿真程序对串口的设置不正确，这包括：波特率、奇偶校验、数据位和停止位等方面的设置。 <br><br>此外，有时也会碰到这样的问题，那就是：在 boot loader 的运行过程中我们可以正确地向串口终端输出信息，但当 boot loader 启动内核后却无法看到内核的启动输出信息。对这一问题的原因可以从以下几个方面来考虑： <br><br>(1) 首先请确认你的内核在编译时配置了对串口终端的支持，并配置了正确的串口驱动程序。 <br>(2) 你的 boot loader 对串口的初始化设置可能会和内核对串口的初始化设置不一致。此外，对于诸如 s3c44b0x 这样的 CPU，CPU 时钟频率的设置也会影响串口，因此如果 boot loader 和内核对其 CPU 时钟频率的设置不一致，也会使串口终端无法正确显示信息。 <br>(3) 最后，还要确认 boot loader 所用的内核基地址必须和内核映像在编译时所用的运行基地址一致，尤其是对于 uClinux 而言。假设你的内核映像在编译时用的基地址是 0xc0008000，但你的 boot loader 却将它加载到 0xc0010000 处去执行，那么内核映像当然不能正确地执行了。 <br><br><strong>五、结束语<br><br></strong>Boot Loader 的设计与实现是一个非常复杂的过程。如果不能从串口收到那激动人心的"uncompressing linux.................. done, booting the kernel&#8230;&#8230;"内核启动信息，恐怕谁也不能说："嗨，我的 boot loader 已经成功地转起来了！"。</font></div>
<img src ="http://www.cppblog.com/yishanhante/aggbug/66774.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/yishanhante/" target="_blank">jay</a> 2008-11-13 00:09 <a href="http://www.cppblog.com/yishanhante/articles/66774.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>uClinux系统简介</title><link>http://www.cppblog.com/yishanhante/articles/65692.html</link><dc:creator>jay</dc:creator><author>jay</author><pubDate>Sat, 01 Nov 2008 06:28:00 GMT</pubDate><guid>http://www.cppblog.com/yishanhante/articles/65692.html</guid><wfw:comment>http://www.cppblog.com/yishanhante/comments/65692.html</wfw:comment><comments>http://www.cppblog.com/yishanhante/articles/65692.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/yishanhante/comments/commentRss/65692.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/yishanhante/services/trackbacks/65692.html</trackback:ping><description><![CDATA[<span  style="font-family: simsun; ">项目主页:&#160;<a href="http://www.uclinux.org/" target="_blank">http://www.uclinux.org/</a></span>

<div><span  style="font-family: simsun; "><p><strong>简介</strong><br>Linux是一种很受欢迎的操作系统，它与UNIX系统兼容，开放源代码。它原本被设计为桌面系统，现在广泛应用于服务器领域。而更大的影响在于它正逐渐的应用于嵌入式设备。uClinux正是在这种氛围下产生的。在uClinux这个英文单词中u表示Micro，小的意思，C表示Control，控制的意思，所以uClinux就是Micro-Control-Linux，字面上的理解就是"针对微控制领域而设计的Linux系统"。<br><br><strong>uClinux小型化的做法</strong><br><br><strong>标准Linux可能采用的小型化方法</strong><br>1. 重新编译内核<br>Linux内核采用模块化的设计，即很多功能块可以独立的加上或卸下，开发人员在设计内核时把这些内核模块作为可选的选项，可以在编译系统内核时指定。因此一种较通用的做法是对Linux内核重新编译，在编译时仔细的选择嵌入式设备所需要的功能支持模块，同时删除不需要的功能。通过对内核的重新配置，可以使系统运行所需要的内核显著减小，从而缩减资源使用量。<br>2. 制作root文件系统映象<br>Linux系统在启动时必须加载根（root）文件系统，因此剪裁系统同时包括root file system的剪裁。在x86系统下，Linux可以在Dos下，使用Loadlin文件加载启动，<br><strong>uClinux采用的小型化方法</strong><br>1．uClinux的内核加载方式<br>uClinux的内核有两种可选的运行方式：可以在flash上直接运行，也可以加载到内存中运行。这种做法可以减少内存需要。<br>Flash运行方式：把内核的可执行映象烧写到flash上，系统启动时从flash的某个地址开始逐句执行。这种方法实际上是很多嵌入式系统采用的方法。<br>内核加载方式：把内核的压缩文件存放在flash上，系统启动时读取压缩文件在内存里解压，然后开始执行，这种方式相对复杂一些，但是运行速度可能更快（ram的存取速率要比flash高）。同时这也是标准Linux系统采用的启动方式。<br>2．uClinux的根（root）文件系统<br>uClinux系统采用romfs文件系统，这种文件系统相对于一般的ext2文件系统要求更少的空间。空间的节约来自于两个方面，首先内核支持romfs文件系统比支持ext2文件系统需要更少的代码，其次romfs文件系统相对简单，在建立文件系统超级块（superblock）需要更少的存储空间。Romfs文件系统不支持动态擦写保存，对于系统需要动态保存的数据采用虚拟ram盘的方法进行处理（ram盘将采用ext2文件系统）。<br>3．uClinux的应用程序库<br>uClinux小型化的另一个做法是重写了应用程序库，相对于越来越大且越来越全的glibc库，uClibc对libc做了精简。<br>uClinux对用户程序采用静态连接的形式，这种做法会使应用程序变大，但是基于内存管理的问题，不得不这样做（这将在下文对uClinux内存管理展开分析时进行说明），同时这种做法也更接近于通常嵌入式系统的做法。<br><br><strong>uClinux的开发环境</strong><br><br><strong>GNU开发套件</strong><br>Gnu开发套件作为通用的Linux开放套件，包括一系列的开发调试工具。主要组件：<br>Gcc： 编译器，可以做成交叉编译的形式，即在宿主机上开发编译目标上可运行的二进制文件。<br>Binutils：一些辅助工具，包括objdump（可以反编译二进制文件），as（汇编编译器），ld（连接器）等等。<br>Gdb：调试器，可使用多种交叉调试方式，gdb-bdm（背景调试工具），gdbserver（使用以太网络调试）。<br>uClinux的打印终端<br>通常情况下，uClinux的默认终端是串口，内核在启动时所有的信息都打印到串口终端（使用printk函数打印），同时也可以通过串口终端与系统交互。<br>uClinux在启动时启动了telnetd（远程登录服务），操作者可以远程登录上系统，从而控制系统的运行。至于是否允许远程登录可以通过烧写romfs文件系统时有用户决定是否启动远程登录服务。<br><strong>交叉编译调试工具</strong><br>支持一种新的处理器，必须具备一些编译，汇编工具，使用这些工具可以形成可运行于这种处理器的二进制文件。对于内核使用的编译工具同应用程序使用的有所不同。在解释不同点之前，需要对gcc连接做一些说明：&#160;<br>.ld（link description）文件：ld文件是指出连接时内存映象格式的文件。<br>crt0.S：应用程序编译连接时需要的启动文件，主要是初始化应用程序栈。<br>pic：position independence code ，与位置无关的二进制格式文件，在程序段中必须包括reloc段，从而使的代码加载时可以进行重新定位。<br>内核编译连接时，使用ucsimm.ld文件，形成可执行文件映象，所形成的代码段既可以使用间接寻址方式（即使用reloc段进行寻址），也可以使用绝对寻址方式。这样可以给编译器更多的优化空间。因为内核可能使用绝对寻址，所以内核加载到的内存地址空间必须与ld文件中给定的内存空间完全相同。&#160;<br>应用程序的连接与内核连接方式不同。应用程序由内核加载（可执行文件加载器将在后面讨论），由于应用程序的ld文件给出的内存空间与应用程序实际被加载的内存位置可能不同，这样在应用程序加载的过程中需要一个重新地位的过程，即对reloc段进行修正，使得程序进行间接寻址时不至于出错。（这个问题在i386等高级处理器上方法有所不同，本文将在后面进一步分析）。<br>由上述讨论，至少需要两套编译连接工具。在讨论过uClinux的内存管理后本文将给出整个系统的工作流程以及系统在flash和ram中的空间分布。<br><strong>可执行文件格式</strong><br>先对一些名词作一些说明：&#160;<br>coff（common object file format）：一种通用的对象文件格式<br>elf（excutive linked file）：一种为Linux系统所采用的通用文件格式，支持动态连接<br>flat：elf格式有很大的文件头，flat文件对文件头和一些段信息做了简化<br>uClinux系统使用flat可执行文件格式，gcc的编译器不能直接形成这种文件格式，但是可以形成coff或elf格式的可执行文件，这两种文件需要coff2flt或elf2flt工具进行格式转化，形成flat文件。<br>当用户执行一个应用时，内核的执行文件加载器将对flat文件进行进一步处理，主要是对reloc段进行修正（可执行文件加载器的详见fs/binfmt_flat.c）。以下对reloc段进一步讨论。<br>需要reloc段的根本原因是，程序在连接时连接器所假定的程序运行空间与实际程序加载到的内存空间不同。假如有这样一条指令：<br>jsr app_start;<br>这一条指令采用直接寻址，跳转到app_start地址处执行，连接程序将在编译完成是计算出app_start的实际地址（设若实际地址为0x10000），这个实际地址是根据ld文件计算出来（因为连接器假定该程序将被加载到由ld文件指明的内存空间）。但实际上由于内存分配的关系，操作系统在加载时无法保证程序将按ld文件加载。这时如果程序仍然跳转到绝对地址0x10000处执行，通常情况这是不正确的。一个解决办法是增加一个存储空间，用于存储app_start的实际地址，设若使用变量addr表示这个存储空间。则以上这句程序将改为：<br>movl addr, a0;<br>jsr (a0);<br>增加的变量addr将在数据段中占用一个4字节的空间，连接器将app_start的绝对地址存储到该变量。在可执行文件加载时，可执行文件加载器根据程序将要加载的内存空间计算出app_start在内存中的实际位置，写入addr变量。系统在实际处理是不需要知道这个变量的确切存储位置（也不可能知道），系统只要对整个reloc段进行处理就可以了（reloc段有标识，系统可以读出来）。处理很简单只需要对reloc段中存储的值统一加上一个偏置（如果加载的空间比预想的要靠前，实际上是减去一个偏移量）。偏置由实际的物理地址起始值同ld文件指定的地址起始值相减计算出。<br>这种reloc的方式部分是由uClinux的内存分配问题引起的，这一点将在uClinux内存管理分析时说明。<br><strong>针对实时性的解决方案</strong><br>uClinux本身并没有关注实时问题，它并不是为了Linux的实时性而提出的。另外有一种Linux--Rt-linux关注实时问题。Rt-linux执行管理器把普通Linux的内核当成一个任务运行，同时还管理了实时进程。而非实时进程则交给普通Linux内核处理。这种方法已经应用于很多的操作系统用于增强操作系统的实时性，包括一些商用版UNIX系统，Windows NT等等。这种方法优点之一是实现简单，且实时性能容易检验。优点之二是由于非实时进程运行于标准Linux系统，同其它Linux商用版本之间保持了很大的兼容性。优点之三是可以支持硬实时时钟的应用。uClinux可以使用Rt-linux的patch，从而增强uClinux的实时性，使得uClinux可以应用于工业控制、进程控制等一些实时要求较高的应用。<br></p><p><strong>uClinux的内存管理</strong><br>应该说uClinux同标准Linux的最大区别就在于内存管理，同时也由于uClinux的内存管理引发了一些标准Linux所不会出现的问题。本文将把uClinux内存管理同标准Linux的那内存管理部分进行比较分析。<br>标准Linux使用的虚拟存储器技术<br>标准Linux使用虚拟存储器技术，这种技术用于提供比计算机系统中实际使用的物理内存大得多的内存空间。使用者将感觉到好像程序可以使用非常大的内存空间，从而使得编程人员在写程序时不用考虑计算机中的物理内存的实际容量。</p><p>为了支持虚拟存储管理器的管理，Linux系统采用分页（paging）的方式来载入进程。所谓分页既是把实际的存储器分割为相同大小的段，例如每个段1024个字节，这样1024个字节大小的段便称为一个页面（page）。&#160;<br>虚拟存储器由存储器管理机制及一个大容量的快速硬盘存储器支持。它的实现基于局部性原理，当一个程序在运行之前，没有必要全部装入内存，而是仅将那些当前要运行的那些部分页面或段装入内存运行（copy-on-write），其余暂时留在硬盘上程序运行时如果它所要访问的页（段）已存在，则程序继续运行，如果发现不存在的页（段），操作系统将产生一个页错误（page fault），这个错误导致操作系统把需要运行的部分加载到内存中。必要时操作系统还可以把不需要的内存页（段）交换到磁盘上。利用这样的方式管理存储器，便可把一个进程所需要用到的存储器以化整为零的方式，视需求分批载入，而核心程序则凭借属于每个页面的页码来完成寻址各个存储器区段的工作。<br>标准Linux是针对有内存管理单元的处理器设计的。在这种处理器上，虚拟地址被送到内存管理单元（MMU），把虚拟地址映射为物理地址。<br>通过赋予每个任务不同的虚拟--物理地址转换映射，支持不同任务之间的保护。地址转换函数在每一个任务中定义，在一个任务中的虚拟地址空间映射到物理内存的一个部分，而另一个任务的虚拟地址空间映射到物理存储器中的另外区域。计算机的存储管理单元（MMU）一般有一组寄存器来标识当前运行的进程的转换表。在当前进程将CPU放弃给另一个进程时（一次上下文切换），内核通过指向新进程地址转换表的指针加载这些寄存器。MMU寄存器是有特权的，只能在内核态才能访问。这就保证了一个进程只能访问自己用户空间内的地址，而不会访问和修改其它进程的空间。当可执行文件被加载时，加载器根据缺省的ld文件，把程序加载到虚拟内存的一个空间，因为这个原因实际上很多程序的虚拟地址空间是相同的，但是由于转换函数不同，所以实际所处的内存区域也不同。而对于多进程管理当处理器进行进程切换并执行一个新任务时，一个重要部分就是为新任务切换任务转换表。我们可以看到Linux系统的内存管理至少实现了以下功能：<br>运行比内存还要大的程序。理想情况下应该可以运行任意大小的程序<br>◇可以运行只加载了部分的程序，缩短了程序启动的时间<br>◇可以使多个程序同时驻留在内存中提高CPU的利用率<br>◇可以运行重定位程序。即程序可以方于内存中的任何一处，而且可以在执行过程中移动。<br>◇写机器无关的代码。程序不必事先约定机器的配置情况。<br>◇减轻程序员分配和管理内存资源的负担。<br>◇可以进行共享--例如，如果两个进程运行同一个程序，它们应该可以共享程序代码的同一个副本。<br>◇提供内存保护，进程不能以非授权方式访问或修改页面，内核保护单个进程的数据和代码以防止其它进程修改它们。否则，用户程序可能会偶然（或恶意）的破坏内核或其它用户程序。&#160;<br>虚存系统并不是没有代价的。内存管理需要地址转换表和其他一些数据结构，留给程序的内存减少了。地址转换增加了每一条指令的执行时间，而对于有额外内存操作的指令会更严重。当进程访问不在内存的页面时，系统发生失效。系统处理该失效，并将页面加载到内存中，这需要极耗时间的磁盘I/O操作。总之内存管理活动占用了相当一部分cpu时间（在较忙的系统中大约占10％）。<br>uClinux针对NOMMU的特殊处理<br>对于uClinux来说，其设计针对没有MMU的处理器，即uClinux不能使用处理器的虚拟内存管理技术（应该说这种不带有MMU的处理器在嵌入式设备中相当普偏）。uClinux仍然采用存储器的分页管理，系统在启动时把实际存储器进行分页。在加载应用程序时程序分页加载。但是由于没有MMU管理，所以实际上uClinux采用实存储器管理策略（real memeory management）。这一点影响了系统工作的很多方面。<br>uClinux系统对于内存的访问是直接的，（它对地址的访问不需要经过MMU，而是直接送到地址线上输出），所有程序中访问的地址都是实际的物理地址。操作系统对内存空间没有保护（这实际上是很多嵌入式系统的特点），各个进程实际上共享一个运行空间（没有独立的地址转换表）。&#160;<br>一个进程在执行前，系统必须为进程分配足够的连续地址空间，然后全部载入主存储器的连续空间中。与之相对应的是标准Linux系统在分配内存时没有必要保证实际物理存储空间是连续的，而只要保证虚存地址空间连续就可以了。另外一个方面程序加载地址与预期（ld文件中指出的）通常都不相同，这样relocation过程就是必须的。此外磁盘交换空间也是无法使用的，系统执行时如果缺少内存将无法通过磁盘交换来得到改善。<br>uClinux对内存的管理减少同时就给开发人员提出了更高的要求。如果从易用性这一点来说，uClinux的内存管理是一种倒退，退回了到了UNIX早期或是Dos系统时代。开发人员不得不参与系统的内存管理。从编译内核开始，开发人员必须告诉系统这块开发板到底拥有多少的内存（假如你欺骗了系统，那将在后面运行程序时受到惩罚），从而系统将在启动的初始化阶段对内存进行分页，并且标记已使用的和未使用的内存。系统将在运行应用时使用这些分页内存。<br>由于应用程序加载时必须分配连续的地址空间，而针对不同硬件平台的可一次成块（连续地址）分配内存大小限制是不同（目前针对ez328处理器的uClinux是128k，而针对coldfire处理器的系统内存则无此限制），所以开发人员在开发应用程序时必须考虑内存的分配情况并关注应用程序需要运行空间的大小。另外由于采用实存储器管理策略，用户程序同内核以及其它用户程序在一个地址空间，程序开发时要保证不侵犯其它程序的地址空间，以使得程序不至于破坏系统的正常工作，或导致其它程序的运行异常。<br>从内存的访问角度来看，开发人员的权利增大了（开发人员在编程时可以访问任意的地址空间），但与此同时系统的安全性也大为下降。此外，系统对多进程的管理将有很大的变化，这一点将在uClinux的多进程管理中说明。<br>虽然uClinux的内存管理与标准Linux系统相比功能相差很多，但应该说这是嵌入式设备的选择。在嵌入式设备中，由于成本等敏感因素的影响，普偏的采用不带有MMU的处理器，这决定了系统没有足够的硬件支持实现虚拟存储管理技术。从嵌入式设备实现的功能来看，嵌入式设备通常在某一特定的环境下运行，只要实现特定的功能，其功能相对简单，内存管理的要求完全可以由开发人员考虑。<br>标准Linux系统的进程、线程<br>进程：进程是一个运行程序并为其提供执行环境的实体，它包括一个地址空间和至少一个控制点，进程在这个地址空间上执行单一指令序列。进程地址空间包括可以访问或引用的内存单元的集合，进程控制点通过一个一般称为程序计数器（program counter,PC）的硬件寄存器控制和跟踪进程指令序列。<br>fork：由于进程为执行程序的环境，因此在执行程序前必须先建立这个能"跑"程序的环境。Linux系统提供系统调用拷贝现行进程的内容，以产生新的进程，调用fork的进程称为父进程；而所产生的新进程则称为子进程。子进程会承袭父进程的一切特性，但是它有自己的数据段，也就是说，尽管子进程改变了所属的变量，却不会影响到父进程的变量值。<br>父进程和子进程共享一个程序段，但是各自拥有自己的堆栈、数据段、用户空间以及进程控制块。换言之，两个进程执行的程序代码是一样的，但是各有各的程序计数器与自己的私人数据。&#160;<br>当内核收到fork请求时，它会先查核三件事：首先检查存储器是不是足够；其次是进程表是否仍有空缺；最后则是看看用户是否建立了太多的子进程。如果上述说三个条件满足，那么操作系统会给子进程一个进程识别码，并且设定cpu时间，接着设定与父进程共享的段，同时将父进程的inode拷贝一份给子进程运用，最终子进程会返回数值0以表示它是子进程，至于父进程，它可能等待子进程的执行结束，或与子进程各做个的。<br>exec系统调用：该系统调用提供一个进程去执行另一个进程的能力，exec系统调用是采用覆盖旧有进程存储器内容的方式，所以原来程序的堆栈、数据段与程序段都会被修改，只有用户区维持不变。<br>vfork系统调用：由于在使用fork时，内核会将父进程拷贝一份给子进程，但是这样的做法相当浪费时间，因为大多数的情形都是程序在调用fork后就立即调用exec，这样刚拷贝来的进程区域又立即被新的数据覆盖掉。因此Linux系统提供一个系统调用vfork，vfork假定系统在调用完成vfork后会马上执行exec，因此vfork不拷贝父进程的页面，只是初始化私有的数据结构与准备足够的分页表。这样实际在vfork调用完成后父子进程事实上共享同一块存储器（在子进程调用exec或是exit之前），因此子进程可以更改父进程的数据及堆栈信息，因此vfork系统调用完成后，父进程进入睡眠，直到子进程执行exec。当子进程执行exec时，由于exec要使用被执行程序的数据，代码覆盖子进程的存储区域，这样将产生写保护错误（do_wp_page）（这个时候子进程写的实际上是父进程的存储区域），<br>这个错误导致内核为子进程重新分配存储空间。当子进程正确开始执行后，将唤醒父进程，使得父进程继续往后执行。<br>uClinux的多进程处理<br>uClinux没有mmu管理存储器，在实现多个进程时（fork调用生成子进程）需要实现数据保护。<br>uClinux的fork和vfork：uClinux的fork等于vfork。实际上uClinux的多进程管理通过vfork来实现。这意味着uClinux系统fork调用完程后，要么子进程代替父进程执行（此时父进程已经sleep）直到子进程调用exit退出，要么调用exec执行一个新的进程，这个时候将产生可执行文件的加载，即使这个进程只是父进程的拷贝，这个过程也不能避免。当子进程执行exit或exec后，子进程使用wakeup把父进程唤醒，父进程继续往下执行。<br>uClinux的这种多进程实现机制同它的内存管理紧密相关。uClinux针对nommu处理器开发，所以被迫使用一种flat方式的内存管理模式，启动新的应用程序时系统必须为应用程序分配存储空间，并立即把应用程序加载到内存。缺少了MMU的内存重映射机制，uClinux必须在可执行文件加载阶段对可执行文件reloc处理，使得程序执行时能够直接使用物理内存。</p></span></div><img src ="http://www.cppblog.com/yishanhante/aggbug/65692.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/yishanhante/" target="_blank">jay</a> 2008-11-01 14:28 <a href="http://www.cppblog.com/yishanhante/articles/65692.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>