<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;fid=6&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / 引导模块和保护模式]]></title>
		<link>http://www.gentoo-zh.org/index.php</link>
		<description><![CDATA[Gentoo中文社区 最近发表的主题。]]></description>
		<lastBuildDate>Wed, 03 Apr 2024 14:04:17 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[U-Boot启动过程--详细版的完全分析]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=855&amp;action=new</link>
			<description><![CDATA[<p>在PC机上引导程序一般由BIOS开始执行，然后读取硬盘中位于MBR(Main Boot Record，主引导记录)中的Bootloader(例如LILO或GRUB),并进一步引导操作系统的启动。</p><p>然而在嵌入式系统中通常没有像BIOS那样的固件程序，因此整个系统的加载启动就完全由bootloader来完成。它主要的功能是加载与引导内核映像 </p><p>一个嵌入式的存储设备通过通常包括四个分区：</p><p>&gt;第一分区：存放的当然是u-boot<br />&gt;第二个分区：存放着u-boot要传给系统内核的参数<br />&gt;第三个分区：是系统内核（kernel）<br />&gt;第四个分区：则是根文件系统</p><p>如下图所示：<br /><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/771569404.png" alt="FluxBB bbcode 测试" /></span></p><p>Bootloader介绍</p><p>u-boot是一种普遍用于嵌入式系统中的Bootloader。</p><p>Bootloader是进行嵌入式开发必然会接触的一个概念</p><p>Bootloader的定义：Bootloader是在操作系统运行之前执行的一小段程序，通过这一小段程序，我们可以初始化硬件设备、建立内存空间的映射表，从而建立适当的系统软硬件环境，为最终调用操作系统内核做好准备。意思就是说如果我们要想让一个操作系统在我们的板子上运转起来，我们就必须首先对我们的板子进行一些基本配置和初始化，然后才可以将操作系统引导进来运行。具体在Bootloader中完成了哪些操作我们会在后面分析到，这里我们先来回忆一下PC的体系结构：PC机中的引导加载程序是由BIOS和位于硬盘MBR中的OS Boot Loader（比如LILO和GRUB等）一起组成的，BIOS在完成硬件检测和资源分配后，将硬盘MBR中的Boot Loader读到系统的RAM中，然后将控制权交给OS Boot Loader。Boot Loader的主要运行任务就是将内核映象从硬盘上读到RAM中，然后跳转到内核的入口点去运行，即开始启动操作系统。在嵌入式系统中，通常并没有像BIOS那样的固件程序（注：有的嵌入式cpu也会内嵌一段短小的启动程序），因此整个系统的加载启动任务就完全由Boot Loader来完成。比如在一个基于ARM7TDMI core的嵌入式系统中，系统在上电或复位时通常都从地址0x00000000处开始执行，而在这个地址处安排的通常就是系统的Boot Loader程序。（先想一下，通用PC和嵌入式系统为何会在此处存在如此的差异呢？）</p><p>Bootloader是基于特定硬件平台来实现的，因此几乎不可能为所有的嵌入式系统建立一个通用的Bootloader，不同的处理器架构都有不同的Bootloader，Bootloader不但依赖于cpu的体系结构，还依赖于嵌入式系统板级设备的配置。对于2块不同的板子而言，即使他们使用的是相同的处理器，要想让运行在一块板子上的Bootloader程序也能运行在另一块板子上，一般也需要修改Bootloader的源程序。</p><p>Bootloader的启动方式</p><p>Bootloader的启动方式主要有网络启动方式、磁盘启动方式和Flash启动方式。</p><p>1、网络启动方式</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/1600058229.png" alt="FluxBB bbcode 测试" /></span></p><p>Bootloader网络启动方式示意图</p><p>如图1所示，里面主机和目标板，他们中间通过网络来连接，首先目标板的DHCP/BIOS通过BOOTP服务来为Bootloader分配IP地址，配置网络参数，这样才能支持网络传输功能。我们使用的u-boot可以直接设置网络参数，因此这里就不用使用DHCP的方式动态分配IP了。接下来目标板的Bootloader通过TFTP服务将内核映像下载到目标板上，然后通过网络文件系统来建立主机与目标板之间的文件通信过程，之后的系统更新通常也是使用Boot Loader的这种工作模式。工作于这种模式下的Boot Loader通常都会向它的终端用户提供一个简单的命令行接口。</p><p>2、磁盘启动方式</p><p>这种方式主要是用在台式机和服务器上的，这些计算机都使用BIOS引导，并且使用磁盘作为存储介质，这里面两个重要的用来启动linux的有LILO和GRUB，这里就不再具体说明了。</p><p>3、Flash启动方式</p><p>这是我们最常用的方式。Flash有NOR Flash和NAND Flash两种。NOR Flash可以支持随机访问，所以代码可以直接在Flash上执行，Bootloader一般是存储在Flash芯片上的。另外Flash上还存储着参数、内核映像和文件系统。这种启动方式与网络启动方式之间的不同之处就在于，在网络启动方式中，内核映像和文件系统首先是放在主机上的，然后经过网络传输下载进目标板的，而这种启动方式中内核映像和文件系统则直接是放在Flash中的，这两点在我们u-boot的使用过程中都用到了。</p><p>U-boot的定义</p><p>U-boot，全称Universal Boot Loader，是由DENX小组的开发的遵循GPL条款的开放源码项目，它的主要功能是完成硬件设备初始化、操作系统代码搬运，并提供一个控制台及一个指令集在操作系统运行前操控硬件设备。U-boot之所以这么通用，原因是他具有很多特点：开放源代码、支持多种嵌入式操作系统内核、支持多种处理器系列、较高的稳定性、高度灵活的功能设置、丰富的设备驱动源码以及较为丰富的开发调试文档与强大的网络技术支持。另外u-boot对操作系统和产品研发提供了灵活丰富的支持，主要表现在：可以引导压缩或非压缩系统内核，可以灵活设置/传递多个关键参数给操作系统，适合系统在不同开发阶段的调试要求与产品发布，支持多种文件系统，支持多种目标板环境参数存储介质，采用CRC32校验，可校验内核及镜像文件是否完好，提供多种控制台接口，使用户可以在不需要ICE的情况下通过串口/以太网/USB等接口下载数据并烧录到存储设备中去（这个功能在实际的产品中是很实用的，尤其是在软件现场升级的时候），以及提供丰富的设备驱动等。</p><p>u-boot源代码的目录结构</p><p>&gt;1、board中存放于开发板相关的配置文件，每一个开发板都以子文件夹的形式出现。<br />&gt;2、Commom文件夹实现u-boot行下支持的命令，每一个命令对应一个文件。<br />&gt;3、cpu中存放特定cpu架构相关的目录，每一款cpu架构都对应了一个子目录。<br />&gt;4、Doc是文档目录，有u-boot非常完善的文档。<br />&gt;5、Drivers中是u-boot支持的各种设备的驱动程序。<br />&gt;6、Fs是支持的文件系统，其中最常用的是JFFS2文件系统。<br />&gt;7、Include文件夹是u-boot使用的头文件，还有各种硬件平台支持的汇编文件，系统配置文件和文件系统支持的文件。<br />&gt;8、Net是与网络协议相关的代码，bootp协议、TFTP协议、NFS文件系统得实现。<br />&gt;9、Tooles是生成U-boot的工具。</p><p>对u-boot的目录有了一些了解后，分析启动代码的过程就方便多了，其中比较重要的目录就是/board、/cpu、/drivers和/include目录，如果想实现u-boot在一个平台上的移植，就要对这些目录进行深入的分析。</p><p>什么是《编译地址》？什么是《运行地址》？</p><p>（一）编译地址： 32位的处理器，它的每一条指令是4个字节，以4个字节存储顺序，进行顺序执行，CPU是顺序执行的，只要没发生什么跳转，它会顺序进行执行行， 编译器会对每一条指令分配一个编译地址，这是编译器分配的，在编译过程中分配的地址，我们称之为编译地址。<br />（二）运行地址：是指程序指令真正运行的地址，是由用户指定，用户将运行地址烧录到哪里，哪里就是运行的地址。</p><p>比如有一个指令的编译地址是0x5，实际运行的地址是0x200，如果用户将指令烧到0x200上，那么这条指令的运行地址就是0x200，当编译地址和运行地址不同的时候会出现什么结果？结果是不能跳转，编译后会产生跳转地址，如果实际地址和编译后产生的地址不相等，那么就不能跳转。</p><p>C语言编译地址：都希望把编译地址和实际运行地址放在一起的，但是汇编代码因为不需要做C语言到汇编的转换，可以认为的去写地址，所以直接写的就是他的运行地址，这就是为什么任何bootloader刚开始会有一段汇编代码，因为起始代码编译地址和实际地址不相等，这段代码和汇编无关，跳转用的运行地址。&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; </p><p>编译地址和运行地址如何来算呢？</p><p>1.假如有两个编译地址a=0x10，b=0x7，b的运行地址是0x300，那么a的运行地址就是b的运行地址加上两者编译地址的差值，a-b=0x10-0x7=0x9， a的运行地址就是0x300+0x9=0x309。</p><p>2.假设uboot上两条指令的编译地址为a=0x33000007和b=0x33000001，这两条指令都落在bank6上，现在要计算出他们对应的运行地址，要找出运行地址的始地址，这个是由用户烧录进去的，假设运行地址的首地址是0x0，则a的运行地址为0x7，b为0x1，就是这样算出来的。</p><p>为什么要分配编译地址？这样做有什么好处，有什么作用？</p><p>比如在函数a中定义了函数b，当执行到函数b时要进行指令跳转，要跳转到b函数所对应的起始地址上去，编译时，编译器给每条指令都分配了编译地址，如果编译器已经给分配了地址就可以直接进行跳转，查找b函数跳转指令所对应的表，进行直接跳转，因为有个编译地址和指令对应的一个表，如果没有分配，编译器就查找不到这个跳转地址，要进行计算，非常麻烦。</p><p>什么是《相对地址》？</p><p>以NOR Flash为例，NOR Falsh是映射到bank0上面，SDRAM是映射到bank6上面，uboot和内核最终是在SDRAM上面运行，最开始我们是从Nor Flash的零地址开始往后烧录，uboot中至少有一段代码编译地址和运行地址是不一样的，编译uboot或内核时，都会将编译地址放入到SDRAM中，他们最终都会在SDRAM中执行，刚开始uboot在Nor Flash中运行，运行地址是一个低端地址，是bank0中的一个地址，但编译地址是bank6中的地址，这样就会导致绝对跳转指令执行的失败，所以就引出了相对地址的概念。</p><p>那么什么是相对地址呢？</p><p>至少在bank0中uboot这段代码要知道不能用b+编译地址这样的方法去跳转指令，因为这段代码的编译地址和运行地址不一样，那如何去做呢？要去计算这个指令运行的真实地址，计算出来后再做跳转，应该是b+运行地址，不能出现b+编译地址，而是b+运行地址，而运行地址是算出来的。</p><div class="codebox"><pre><code>   _TEXT_BASE:
  .word TEXT_BASE //0x33F80000,  // 在board/config.mk中</code></pre></div><br /><p>这段话表示，用户告诉编译器编译地址的起始地址</p><p>uboot 工作过程</p><p>大多数 Boot Loader 都包含两种不同的操作模式:&quot;启动加载&quot;模式和&quot;下载&quot;模式,这种区别仅对于开发人员才有意义。</p><p>但从最终用户的角度看,Boot Loader 的作用就是:用来加载操作系统,而并不存在所谓的启动加载模式与下载工作模式的区别。</p><p>（一）启动加载(Boot loading)模式:这种模式也称为&quot;自主&quot;(Autonomous)模式。<br />也即 Boot Loader 从目标机上的某个固态存储设备上将操作系统加载到 RAM 中运行,整个过程并没有用户的介入。这种模式是 Boot Loader 的正常工作模式,因此在嵌入式产品发布的时侯,Boot Loader 显然必须工作在这种模式下。</p><br /><p>（二）下载(Downloading)模式:<br />在这种模式下,目标机上的 Boot Loader 将通过串口连接或网络连接等通信手段从主机(Host)下载文件,比如:下载内核映像和根文件系统映像等。从主机下载的文件通常首先被 Boot Loader保存到目标机的RAM 中,然后再被 BootLoader写到目标机上的FLASH类固态存储设备中。Boot Loader 的这种模式通常在第一次安装内核与根文件系统时被使用;此外,以后的系统更新也会使用 Boot Loader 的这种工作模式。工作于这种模式下的 Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口。这种工作模式通常在第一次安装内核与跟文件系统时使用。或者在系统更新时使用。进行嵌入式系统调试时一般也让bootloader工作在这一模式下。</p><p>U­Boot 这样功能强大的 Boot Loader 同时支持这两种工作模式,而且允许用户在这两种工作模式之间进行切换。</p><p>大多数 bootloader 都分为阶段 1(stage1)和阶段 2(stage2)两大部分,u­boot 也不例外。依赖于 CPU 体系结构的代码(如 CPU 初始化代码等)通常都放在阶段 1 中且通常用汇编语言实现,而阶段 2 则通常用 C 语言来实现,这样可以实现复杂的功能,而且有更好的可读性和移植性。</p><p>第一、大概总结性得的分析</p><p>系统启动的入口点。既然我们现在要分析u-boot的启动过程，就必须先找到u-boot最先实现的是哪些代码，最先完成的是哪些任务。</p><p>另一方面一个可执行的image必须有一个入口点，并且只能有一个全局入口点，所以要通知编译器这个入口在哪里。由此我们可以找到程序的入口点是在/board/lpc2210/u-boot.lds中指定的，其中ENTRY(_start)说明程序从_start开始运行，而他指向的是cpu/arm7tdmi/start.o文件。</p><p>因为我们用的是ARM7TDMI的cpu架构，在复位后从地址0x00000000取它的第一条指令，所以我们将Flash映射到这个地址上，</p><p>这样在系统加电后，cpu将首先执行u-boot程序。u-boot的启动过程是多阶段实现的，分了两个阶段。</p><p>依赖于cpu体系结构的代码（如设备初始化代码等）通常都放在stage1中，而且通常都是用汇编语言来实现，以达到短小精悍的目的。</p><p>而stage2则通常是用C语言来实现的，这样可以实现复杂的功能，而且代码具有更好的可读性和可移植性。</p><p>下面我们先详细分析下stage1中的代码，如图2所示：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/3177578898.png" alt="FluxBB bbcode 测试" /></span></p><p>代码真正开始是在_start，设置异常向量表，这样在cpu发生异常时就跳转到/cpu/arm7tdmi/interrupts中去执行相应得中断代码。</p><p>在interrupts文件中大部分的异常代码都没有实现具体的功能，只是打印一些异常消息，其中关键的是reset中断代码，跳到reset入口地址。</p><p>reset复位入口之前有一些段的声明。</p><p>&gt;1.在reset中，首先是将cpu设置为svc32模式下，并屏蔽所有irq和fiq。<br />&gt;2.在u-boot中除了定时器使用了中断外，其他的基本上都不需要使用中断，比如串口通信和网络等通信等，在u-boot中只要完成一些简单的通信就可以了，所以在这里屏蔽掉了所有的中断响应。<br />&gt;3.初始化外部总线。这部分首先设置了I/O口功能，包括串口、网络接口等的设置，其他I/O口都设置为GPIO。然后设置BCFG0~BCFG3，即外部总线控制器。这里bank0对应Flash，设置为16位宽度，总线速度设为最慢，以实现稳定的操作；Bank1对应DRAM，设置和Flash相同；Bank2对应RTL8019。<br />&gt;4.接下来是cpu关键设置，包括系统重映射（告诉处理器在系统发生中断的时候到外部存储器中去读取中断向量表）和系统频率。<br />&gt;5.lowlevel_init，设定RAM的时序，并将中断控制器清零。这些部分和特定的平台有关，但大致的流程都是一样的。</p><p>下面就是代码的搬移阶段了。为了获得更快的执行速度，通常把stage2加载到RAM空间中来执行，因此必须为加载Boot Loader的stage2准备好一段可用的RAM空间范围。空间大小最好是memory page大小(通常是4KB)的倍数一般而言，1M的RAM空间已经足够了。</p><p>flash中存储的u-boot可执行文件中，代码段、数据段以及BSS段都是首尾相连存储的，所以在计算搬移大小的时候就是利用了用BSS段的首地址减去代码的首地址，这样算出来的就是实际使用的空间。</p><p>程序用一个循环将代码搬移到0x81180000，即RAM底端1M空间用来存储代码。</p><p>然后程序继续将中断向量表搬到RAM的顶端。由于stage2通常是C语言执行代码，所以还要建立堆栈去。</p><p>在堆栈区之前还要将malloc分配的空间以及全局数据所需的空间空下来，他们的大小是由宏定义给出的，可以在相应位置修改。</p><p>基本内存分布图：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/3845885394.png" alt="FluxBB bbcode 测试" /></span></p><p>下来是u-boot启动的第二个阶段，是用c代码写的，这部分是一些相对变化不大的部分，我们针对不同的板子改变它调用的一些初始化函数，并且通过设置一些宏定义来改变初始化的流程，所以这些代码在移植的过程中并不需要修改，也是错误相对较少出现的文件。在文件的开始先是定义了一个函数指针数组，通过这个数组，程序通过一个循环来按顺序进行常规的初始化，并在其后通过一些宏定义来初始化一些特定的设备。在最后程序进入一个循环，main_loop。这个循环接收用户输入的命令，以设置参数或者进行启动引导。</p><p>本篇文章将分析重点放在了前面的start.s上，是因为这部分无论在移植还是在调试过程中都是最容易出问题的地方，要解决问题就需要程序员对代码进行修改，所以在这里简单介绍了一下start.s的基本流程，希望能对大家有所帮助</p><p>第二、代码分析</p><p>u­boot 的 stage1 代码通常放在 start.s 文件中,它用汇编语言写成</p><p>由于一个可执行的 Image 必须有一个入口点,并且只能有一个全局入口,通常这个入口放在 ROM(Flash)的 0x0地址,因此,必须通知编译器以使其知道这个入口,该工作可通过修改连接器脚本来完成。</p><p>1. board/crane2410/u­boot.lds:&#160; ENTRY(_start)&#160; &#160;==&gt; cpu/arm920t/start.S: .globl _start<br />2. uboot 代码区(TEXT_BASE = 0x33F80000)定义在 board/crane2410/config.mk</p><p>U-Boot启动内核的过程可以分为两个阶段，两个阶段的功能如下：</p><p>（1）第一阶段的功能<br />&gt;Ø&#160; 硬件设备初始化<br />&gt;Ø&#160; 加载U-Boot第二阶段代码到RAM空间<br />&gt;Ø&#160; 设置好栈<br />&gt;Ø&#160; 跳转到第二阶段代码入口</p><p>（2）第二阶段的功能<br />&gt;Ø&#160; 初始化本阶段使用的硬件设备<br />&gt;Ø&#160; 检测系统内存映射<br />&gt;Ø&#160; 将内核从Flash读取到RAM中<br />&gt;Ø&#160; 为内核设置启动参数<br />&gt;Ø&#160; 调用内核</p><p>Uboot启动第一阶段代码分析</p><p>第一阶段对应的文件是cpu/arm920t/start.S和board/samsung/mini2440/lowlevel_init.S。</p><p>U-Boot启动第一阶段流程如下：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/902681701.png" alt="FluxBB bbcode 测试" /></span></p><p>详细分析</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/236181652.png" alt="FluxBB bbcode 测试" /></span></p><p>根据cpu/arm920t/u-boot.lds中指定的连接方式：</p><p>看一下uboot.lds文件，在board/smdk2410目录下面，uboot.lds是告诉编译器这些段改怎么划分，GUN编译过的段，最基本的三个段是RO，RW，ZI，RO表示只读，对应于具体的指代码段，RW是数据段，ZI是归零段，就是全局变量的那段，Uboot代码这么多，如何保证start.s会第一个执行，编译在最开始呢？就是通过uboot.lds链接文件进行</p><br /><div class="codebox"><pre class="vscroll"><code>OUTPUT_FORMAT(&quot;elf32-littlearm&quot;, &quot;elf32-littlearm&quot;, &quot;elf32-littlearm&quot;)
/*OUTPUT_FORMAT(&quot;elf32-arm&quot;, &quot;elf32-arm&quot;, &quot;elf32-arm&quot;)*/
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000; //起始地址
 
. = ALIGN(4); //4字节对齐
.text : //test指代码段，上面3行标识是不占用任何空间的
{
cpu/arm920t/start.o (.text) //这里把start.o放在第一位就表示把start.s编
译时放到最开始，这就是为什么把uboot烧到起始地址上它肯定运行的是start.s
*(.text)
}
 
. = ALIGN(4); //前面的 “.” 代表当前值，是计算一个当前的值，是计算上
面占用的整个空间，再加一个单元就表示它现在的位置
.rodata : { *(.rodata) }
 
. = ALIGN(4);
.data : { *(.data) }
 
. = ALIGN(4);
.got : { *(.got) }
 
. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
 
. = ALIGN(4);
__bss_start = .; //bss表示归零段
.bss : { *(.bss) }
_end = .;
}</code></pre></div><p>第一个链接的是cpu/arm920t/start.o，因此u-boot.bin的入口代码在cpu/arm920t/start.o中，其源代码在cpu/arm920t/start.S中。下面我们来分析cpu/arm920t/start.S的执行。</p><p>1.硬件设备初始化</p><p>（1）设置异常向量</p><p>下面代码是系统启动后U-boot上电后运行的第一段代码，它是什么意思？</p><p>u-boot对应的第一阶段代码放在cpu/arm920t/start.S文件中，入口代码如下：</p><br /><div class="codebox"><pre><code>globl _startglobal   /*声明一个符号可被其它文件引用，相当于声明了一个全局变量，.globl与.global相同*/
_start: b start_code /* 复位 */ //b是不带返回的跳转(bl是带返回的跳转)，意思是无条件直接跳转到start_code标号出执行程序
 
ldr pc, _undefined_instruction /*未定义指令向量 l---dr相当于mov操作*/
ldr pc, _software_interrupt /* 软件中断向量 */
ldr pc, _prefetch_abort /* 预取指令异常向量 */
ldr pc, _data_abort /* 数据操作异常向量 */
ldr pc, _not_used /* 未使用 */
ldr pc, _irq /* irq中断向量 */
ldr pc, _fiq /* fiq中断向量 */
 
/* 中断向量表入口地址 */
 
_undefined_instruction: .word undefined_instruction /*就是在当前地址，_undefined_instruction 处存放 undefined_instruction*/
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq
 
 
// word伪操作用于分配一段字内存单元(分配的单元都是字对齐的)，并用伪操作中的expr初始化
.balignl 16,0xdeadbeef</code></pre></div><p>它们是系统定义的异常，一上电程序跳转到start_code异常处执行相应的汇编指令，下面定义出的都是不同的异常，比如软件发生软中断时，CPU就会去执行软中断的指令，这些异常中断在CUP中地址是从0开始，每个异常占4个字节</p><p>ldr pc, _undefined_instruction表示把_undefined_instruction存放的数值存放到pc指针上</p><p>_undefined_instruction: .word undefined_instruction表示未定义的这个异常是由.word来定义的，它表示定义一个字，一个32位的数</p><p>. word后面的数：表示把该标识的编译地址写入当前地址，标识是不占用任何指令的。把标识存放的数值copy到指针pc上面，那么标识上存放的值是什么？</p><p>是由.word undefined_instruction来指定的，pc就代表你运行代码的地址，实现了CPU要做一次跳转时的工作。</p><p>以上代码设置了ARM异常向量表，各个异常向量介绍如下：</p><p>表 2.1 ARM异常向量表</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2931655147.png" alt="FluxBB bbcode 测试" /></span></p><p>在cpu/arm920t/start.S中还有这些异常对应的异常处理程序。当一个异常产生时，CPU根据异常号在异常向量表中找到对应的异常向量，然后执行异常向量处的跳转指令，CPU就跳转到对应的异常处理程序执行。</p><p>其中复位异常向量的指令“b start_code”决定了U-Boot启动后将自动跳转到标号“start_code”处执行。</p><p>（2）CPU进入SVC模式</p><br /><div class="codebox"><pre><code>start_code:
 
/*
* set the cpu to SVC32 mode
*/
 
 mrs r0, cpsr
 
 bic  r0, r0, #0x1f   /*工作模式位清零 */
 
 orr   r0, r0, #0xd3  /*工作模式位设置为“10011”（管理模式），并将中断禁止位和快中断禁止位置1 */
 
 msr cpsr, r0</code></pre></div><p>以上代码将CPU的工作模式位设置为管理模式，即设置相应的CPSR程序状态字，并将中断禁止位和快中断禁止位置一，从而屏蔽了IRQ和FIQ中断。</p><p>操作系统先注册一个总的中断，然后去查是由哪个中断源产生的中断，再去查用户注册的中断表，查出来后就去执行用户定义的用户中断处理函数。</p><p>（3）设置控制寄存器地址</p><br /><div class="codebox"><pre><code>#if defined(CONFIG_S3C2400)       /* 关闭看门狗 */
 
#define pWTCON 0x15300000         /* 看门狗寄存器 */
 
#define INTMSK  0x14400008        /* 中断屏蔽寄存器 */
 
#define CLKDIVN      0x14800014   /* 时钟分频寄存器 */
 
#else      /* s3c2410与s3c2440下面4个寄存器地址相同 */
 
#define pWTCON 0x53000000         /* WATCHDOG控制寄存器地址 */
 
#define INTMSK  0x4A000008        /* INTMSK寄存器地址  */
 
#define INTSUBMSK 0x4A00001C      /* INTSUBMSK寄存器地址 次级中断屏蔽寄存器*/
 
#define CLKDIVN  0x4C000014       /* CLKDIVN寄存器地址 ;时钟分频寄存器*/
 
#endif</code></pre></div><p>对与s3c2440开发板，以上代码完成了WATCHDOG，INTMSK，INTSUBMSK，CLKDIVN四个寄存器的地址的设置。各个寄存器地址参见参考文献</p><p>（4）关闭看门狗</p><br /><div class="codebox"><pre><code>ldr   r0, =pWTCON   /* 将pwtcon寄存器地址赋给R0 */
 
mov   r1, #0x0      /* r1的内容为0 */
 
str   r1, [r0]      /* 看门狗控制器的最低位为0时，看门狗不输出复位信号 */</code></pre></div><p>以上代码向看门狗控制寄存器写入0，关闭看门狗。否则在U-Boot启动过程中，CPU将不断重启。</p><p>为什么要关看门狗？</p><p>就是防止，不同得两个以上得CPU，进行喂狗的时间间隔问题：说白了，就是你运行的代码如果超出喂狗时间，而你不关狗，就会导致，你代码还没运行完又得去喂狗，就这样反复得重启CPU，那你代码永远也运行不完，所以，得先关看门狗得原因，就是这样。</p><p>关狗---详细的原因：</p><p>关闭看门狗，关闭中断，所谓的喂狗是每隔一段时间给某个寄存器置位而已，在实际中会专门启动一个线程或进程会专门喂狗，当上层软件出现故障时就会停止喂狗，停止喂狗之后，cpu会自动复位，一般都在外部专门有一个看门狗，做一个外部的电路，不在cpu内部使用看门狗，cpu内部的看门狗是复位的cpu，当开发板很复杂时，有好几个cpu时，就不能完全让板子复位，但我们通常都让整个板子复位。看门狗每隔短时间就会喂狗，问题是在两次喂狗之间的时间间隔内，运行的代码的时间是否够用，两次喂狗之间的代码是否在两次喂狗的时间延迟之内，如果在延迟之外的话，代码还没运行完就又进行喂狗，代码永远也运行不完</p><p>（5）屏蔽中断</p><br /><div class="codebox"><pre><code>/*
 * mask all IRQs by setting all bits in the INTMR - default
 */
 
 mov       r1, #0xffffffff    /*屏蔽所有中断， 某位被置1则对应的中断被屏蔽 */ /*寄存器中的值*/
 
 ldr   r0, =INTMSK            /*将管理中断的寄存器地址赋给ro*/
 
 str   r1, [r0]               /*将全r1的值赋给ro地址中的内容*/</code></pre></div><p>INTMSK是主中断屏蔽寄存器，每一位对应SRCPND（中断源引脚寄存器）中的一位，表明SRCPND相应位代表的中断请求是否被CPU所处理。</p><p>INTMSK寄存器是一个32位的寄存器，每位对应一个中断，向其中写入0xffffffff就将INTMSK寄存器全部位置一，从而屏蔽对应的中断。</p><br /><div class="codebox"><pre><code># if defined(CONFIG_S3C2440)
 
  ldr  r1, =0x7fff                  
 
  ldr  r0, =INTSUBMSK  
 
  str  r1, [r0]            
 
 # endif</code></pre></div><p>INTSUBMSK每一位对应SUBSRCPND中的一位，表明SUBSRCPND相应位代表的中断请求是否被CPU所处理。</p><p>INTSUBMSK寄存器是一个32位的寄存器，但是只使用了低15位。向其中写入0x7fff就是将INTSUBMSK寄存器全部有效位（低15位）置一，从而屏蔽对应的中断。</p><p>屏蔽所有中断，为什么要关中断？</p><p>中断处理中ldr pc是将代码的编译地址放在了指针上，而这段时间还没有搬移代码，所以编译地址上面没有这个代码，如果进行跳转就会跳转到空指针上面</p><p>（6）设置MPLLCON,UPLLCON, CLKDIVN</p><br /><div class="codebox"><pre class="vscroll"><code># if defined(CONFIG_S3C2440) 
 
#define MPLLCON   0x4C000004
 
#define UPLLCON   0x4C000008  
 
  ldr  r0, =CLKDIVN   ;设置时钟
 
  mov  r1, #5
 
  str  r1, [r0]
 
 
  ldr  r0, =MPLLCON
 
  ldr  r1, =0x7F021 
 
  str  r1, [r0]
 
 
 
  ldr  r0, =UPLLCON 
 
  ldr  r1, =0x38022
 
  str  r1, [r0]
 
# else
 
   /* FCLK:HCLK:PCLK = 1:2:4 */
 
   /* default FCLK is 120 MHz ! */
 
   ldr   r0, =CLKDIVN
 
   mov       r1, #3
 
   str   r1, [r0]
 
#endif</code></pre></div><p>CPU上电几毫秒后，晶振输出稳定，FCLK=Fin（晶振频率），CPU开始执行指令。但实际上，FCLK可以高于Fin，为了提高系统时钟，需要用软件来启用PLL。这就需要设置CLKDIVN，MPLLCON，UPLLCON这3个寄存器。</p><p>CLKDIVN寄存器用于设置FCLK，HCLK，PCLK三者间的比例，可以根据表2.2来设置。</p><p>表 2.2 S3C2440 的CLKDIVN寄存器格式</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/990709079.png" alt="FluxBB bbcode 测试" /></span></p><p>设置CLKDIVN为5，就将HDIVN设置为二进制的10，由于CAMDIVN[9]没有被改变过，取默认值0，因此HCLK = FCLK/4。PDIVN被设置为1，因此PCLK= HCLK/2。因此分频比FCLK:HCLK:PCLK = 1:4:8 。</p><p>MPLLCON寄存器用于设置FCLK与Fin的倍数。MPLLCON的位[19:12]称为MDIV，位[9:4]称为PDIV，位[1:0]称为SDIV。</p><p>对于S3C2440，FCLK与Fin的关系如下面公式：</p><p>&#160; &#160; &#160; &#160;MPLL(FCLK) = (2×m×Fin)/(p× )</p><p>&#160; &#160; &#160; &#160;其中： m=MDIC+8，p=PDIV+2，s=SDIV</p><p>MPLLCON与UPLLCON的值可以根据“PLL VALUE SELECTION TABLE”设置。部分摘录如下：</p><p>表 2.3 推荐PLL值</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/3178107288.png" alt="FluxBB bbcode 测试" /></span></p><p>当mini2440系统主频设置为405MHZ，USB时钟频率设置为48MHZ时，系统可以稳定运行，因此设置MPLLCON与UPLLCON为：</p><p>&#160; &#160; &#160; &#160;MPLLCON=(0x7f&lt;&lt;12) | (0x02&lt;&lt;4) | (0x01) = 0x7f021</p><p>&#160; &#160; &#160; &#160;UPLLCON=(0x38&lt;&lt;12) | (0x02&lt;&lt;4) | (0x02) = 0x38022</p><p>默认频率为&#160; &#160; &#160; FCLK:HCLK:PCLK = 1:2:4,默认 FCLK 的值为 120 MHz,该值为 S3C2410 手册的推荐值。</p><p>设置时钟分频，为什么要设置时钟？</p><p>起始可以不设，系统能不能跑起来和频率没有任何关系，频率的设置是要让外围的设备能承受所设置的频率，如果频率过高则会导致cpu操作外围设备失败</p><p>说白了：设置频率，就为了CPU能去操作外围设备</p><p>（7）关闭MMU，cache（也就是做bank的设置）</p><br /><div class="codebox"><pre><code>#ifndef CONFIG_SKIP_LOWLEVEL_INIT
 
bl   cpu_init_crit  /* ;跳转并把转移后面紧接的一条指令地址保存到链接寄存器LR(R14)中，以此来完成子程序的调用*/
 
#endif</code></pre></div><p>cpu_init_crit 这段代码在U-Boot正常启动时才需要执行，若将U-Boot从RAM中启动则应该注释掉这段代码。</p><p>下面分析一下cpu_init_crit到底做了什么：</p><br /><div class="codebox"><pre class="vscroll"><code>#ifndef CONFIG_SKIP_LOWLEVEL_INIT
 
cpu_init_crit:
 
 /*
 
 * 使数据cache与指令cache无效 */
 
 */
 
 mov r0, #0
 
 mcr p15, 0, r0, c7, c7, 0 /* 向c7写入0将使ICache与DCache无效*/
 
 mcr p15, 0, r0, c8, c7, 0 /* 向c8写入0将使TLB失效 ，协处理器*/
 
 
 
 /*
 
 * disable MMU stuff and caches
 
 */
 
 mrc p15, 0, r0, c1, c0, 0 /* 读出控制寄存器到r0中 */
 
 bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
 
 bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
 
 orr r0, r0, #0x00000002 @ set bit 2 (A) Align
 
 orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
 
 mcr p15, 0, r0, c1, c0, 0 /* 保存r0到控制寄存器 */
 
 /*
 
 * before relocating, we have to setup RAM timing
 
 * because memory timing is board-dependend, you will
 
 * find a lowlevel_init.S in your board directory.
 
 */
 
 mov ip, lr
 
 bl lowlevel_init
 
 mov lr, ip
 
 mov pc, lr
 
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */</code></pre></div><p>代码中的c0，c1，c7，c8都是ARM920T的协处理器CP15的寄存器。其中c7是cache控制寄存器，c8是TLB控制寄存器。325~327行代码将0写入c7、c8，使Cache，TLB内容无效。</p><p>disable MMU stuff and caches 代码关闭了MMU。这是通过修改CP15的c1寄存器来实现的，先看CP15的c1寄存器的格式（仅列出代码中用到的位）：</p><p>表 2.3 CP15的c1寄存器格式（部分）</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/1970327686.png" alt="FluxBB bbcode 测试" /></span></p><p>各个位的意义如下：</p><p>&gt;V :&#160; 表示异常向量表所在的位置，0：异常向量在0x00000000；1：异常向量在 0xFFFF0000<br />&gt;I :&#160; 0 ：关闭ICaches；1 ：开启ICaches<br />&gt;R、S : 用来与页表中的描述符一起确定内存的访问权限<br />&gt;B :&#160; 0 ：CPU为小字节序；1 ： CPU为大字节序<br />&gt;C :&#160; 0：关闭DCaches；1：开启DCaches<br />&gt;A :&#160; 0：数据访问时不进行地址对齐检查；1：数据访问时进行地址对齐检查<br />&gt;M :&#160; 0：关闭MMU；1：开启MMU</p><p>代码将c1的 M位置零，关闭了MMU。</p><p>为什么要关闭catch和MMU呢？catch和MMU是做什么用的？</p><p>MMU是Memory Management Unit的缩写，中文名是内存管理单元，它是中央处理器（CPU）中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址，以及提供硬件机制的内存访问授权&#160; &#160; &#160; </p><p>概述：</p><p>一，关catch</p><p>catch和MMU是通过CP15管理的，刚上电的时候，CPU还不能管理它们，上电的时候MMU必须关闭，指令catch可关闭，可不关闭，但数据catch一定要关闭。否则可能导致刚开始的代码里面，去取数据的时候，从catch里面取，而这时候RAM中数据还没有catch过来，导致数据预取异常</p><p>二：关MMU</p><p>因为MMU是;把虚拟地址转化为物理地址得作用，而目的是设置控制寄存器，而控制寄存器本来就是实地址（物理地址），再使能MMU，不就是多此一举了吗？</p><p>详细分析</p><p>Catch是cpu内部的一个2级缓存，它的作用是将常用的数据和指令放在cpu内部，MMU是用来把虚实地址转换为物理地址用的</p><p>我们的目的:是设置控制的寄存器，寄存器都是实地址（物理地址），如果既要开启MMU又要做虚实地址转换的话，中间还多一步，多此一举了嘛?</p><p>先要把实地址转换成虚地址，然后再做设置，但对uboot而言就是起到一个简单的初始化的作用和引导操作系统，如果开启MMU的话，很麻烦，也没必要，所以关闭MMU.</p><p>说到catch就必须提到一个关键字 Volatile，以后在设置寄存器时会经常遇到，他的本质：是告诉编译器不要对我的代码进行优化，作用是让编写者感觉不到变量的变化情况（也就是说，让它执行速度加快吧）</p><p>优化的过程：是将常用的代码取出来放到catch中，它没有从实际的物理地址去取，它直接从cpu的缓存中去取，但常用的代码就是为了感觉一些常用变量的变化</p><p>优化原因：如果正在取数据的时候发生跳变，那么就感觉不到变量的变化了，所以在这种情况下要用Volatile关键字告诉编译器不要做优化，每次从实际的物理地址中去取指令，这就是为什么关闭catch关闭MMU。</p><p>但在C语言中是不会关闭catch和MMU的，会打开，如果编写者要感觉外界变化，或变化太快，从catch中取数据会有误差，就加一个关键字Volatile。</p><p>（8）初始化RAM控制寄存器</p><p>bl lowlevel_init下来初始化各个bank，把各个bank设置必须搞清楚，对以后移植复杂的uboot有很大帮助，设置完毕后拷贝uboot代码到4k空间，拷贝完毕后执行内存中的uboot代码</p><p>其中的lowlevel_init就完成了内存初始化的工作，由于内存初始化是依赖于开发板的，因此lowlevel_init的代码一般放在board下面相应的目录中。对于mini2440，lowlevel_init在board/samsung/mini2440/lowlevel_init.S中定义如下：</p><br /><div class="codebox"><pre class="vscroll"><code>#define BWSCON 0x48000000 /* 13个存储控制器的开始地址 */
 
 _TEXT_BASE:
 
  .word TEXT_BASE0x33F80000, board/config.mk中这段话表示，用户告诉编译器编译地址的起始地址
 
 
 
 .globl lowlevel_init
 
 lowlevel_init:
 
  /* memory control configuration */
 
  /* make r0 relative the current location so that it */
 
  /* reads SMRDATA out of FLASH rather than memory ! */
 
  ldr r0, =SMRDATA
 
  ldr r1, _TEXT_BASE
 
  sub r0, r0, r1 /* SMRDATA减 _TEXT_BASE就是13个寄存器的偏移地址 */
 
  ldr r1, =BWSCON /* Bus Width Status Controller */
 
  add r2, r0, #13*4
 
 0:
 
  ldr r3, [r0], #4 /*将13个寄存器的值逐一赋值给对应的寄存器*/
 
  str r3, [r1], #4
 
  cmp r2, r0
 
  bne 0b
 
  /* everything is fine now */
 
  mov pc, lr
 
  .ltorg
 
 /* the literal pools origin */
 
 
 SMRDATA: /* 下面是13个寄存器的值 */
 
  .word ...
 
  .word ...
 
...
 
 
 lowlevel_init初始化了13个寄存器来实现RAM时钟的初始化。lowlevel_init函数对于U-Boot从NAND Flash或NOR Flash启动的情况都是有效的。
 
 U-Boot.lds链接脚本有如下代码：
 
 .text :
 {
 
   cpu/arm920t/start.o (.text)
   board/samsung/mini2440/lowlevel_init.o (.text)
   board/samsung/mini2440/nand_read.o (.text)
 
   ...
 }</code></pre></div><p>board/samsung/mini2440/lowlevel_init.o将被链接到cpu/arm920t/start.o后面，因此board/samsung/mini2440/lowlevel_init.o也在U-Boot的前4KB的代码中。</p><p>U-Boot在NAND Flash启动时，lowlevel_init.o将自动被读取到CPU内部4KB的内部RAM中。因此/* reads SMRDATA out of FLASH rather than memory ! */ 开始行的代码将从CPU内部RAM中复制寄存器的值到相应的寄存器中。</p><p>对于U-Boot在NOR Flash启动的情况，由于U-Boot连接时确定的地址是U-Boot在内存中的地址，而此时U-Boot还在NOR Flash中，因此还需要在NOR Flash中读取数据到RAM中。</p><p>由于NOR Flash的开始地址是0，而U-Boot的加载到内存的起始地址是TEXT_BASE，SMRDATA标号在Flash的地址就是SMRDATA－TEXT_BASE。</p><p>综上所述，lowlevel_init的作用就是将SMRDATA开始的13个值复制给开始地址[BWSCON]的13个寄存器，从而完成了存储控制器的设置。</p><p>问题一：如果换一块开发板有可能改哪些东西？</p><p>首先，cpu的运行模式，如果需要对cpu进行设置那就设置，管看门狗，关中断不用改，时钟有可能要改，如果能正常使用则不用改，关闭catch和MMU不用改，设置bank有可能要改。最后一步拷贝时看地址会不会变，如果变化也要改，执行内存中代码，地址有可能要改。</p><br /><p>问题二：Nor Flash和Nand Flash本质区别：</p><p>就在于是否进行代码拷贝，也就是下面代码所表述：无论是Nor Flash还是Nand Flash，核心思想就是将uboot代码搬运到内存中去运行，但是没有拷贝bss后面这段代码，只拷贝bss前面的代码，bss代码是放置全局变量的。Bss段代码是为了清零，拷贝过去再清零重复操作</p><p>（9）复制U-Boot第二阶段代码到RAM</p><p>cpu/arm920t/start.S原来的代码是只支持从NOR Flash启动的，经过修改现在U-Boot在NOR Flash和NAND Flash上都能启动了，实现的思路是这样的：</p><br /><div class="codebox"><pre><code> bl bBootFrmNORFlash /* 判断U-Boot是在NAND Flash还是NOR Flash启动 */
 
 cmp r0, #0 /* r0存放bBootFrmNORFlash函数返回值，若返回0表示NAND Flash启动，否则表示在NOR Flash启动 */
 
 beq nand_boot /* 跳转到NAND Flash启动代码 */
 
 
 /* NOR Flash启动的代码 */
 
 b stack_setup /* 跳过NAND Flash启动的代码 */
 
 
nand_boot:
 
/* NAND Flash启动的代码 */
 
 
stack_setup:
 
 /* 其他代码 */</code></pre></div><p>其中bBootFrmNORFlash函数作用是判断U-Boot是在NAND Flash启动还是NOR Flash启动，若在NOR Flash启动则返回1，否则返回0。根据ATPCS规则，函数返回值会被存放在r0寄存器中，因此调用bBootFrmNORFlash函数后根据r0的值就可以判断U-Boot在NAND Flash启动还是NOR Flash启动。bBootFrmNORFlash函数在board/samsung/mini2440/nand_read.c中定义如下：</p><br /><div class="codebox"><pre><code>int bBootFrmNORFlash(void)
{
    volatile unsigned int *pdw = (volatile unsigned int *)0;
    unsigned int dwVal;
 
 
    dwVal = *pdw;         /* 先记录下原来的数据 */
    *pdw = 0x12345678;
 
    if (*pdw != 0x12345678) /* 写入失败，说明是在NOR Flash启动 */
    {
        return 1;     
    }
    else                   /* 写入成功，说明是在NAND Flash启动 */
    {
        *pdw = dwVal;      /* 恢复原来的数据 */
        return 0;
    }
}</code></pre></div><p>无论是从NOR Flash还是从NAND Flash启动，地址0处为U-Boot的第一条指令“ b&#160; &#160; start_code”。</p><p>对于从NAND Flash启动的情况，其开始4KB的代码会被自动复制到CPU内部4K内存中，因此可以通过直接赋值的方法来修改。</p><p>对于从NOR Flash启动的情况，NOR Flash的开始地址即为0，必须通过一定的命令序列才能向NOR Flash中写数据，所以可以根据这点差别来分辨是从NAND Flash还是NOR Flash启动：向地址0写入一个数据，然后读出来，如果发现写入失败的就是NOR Flash，否则就是NAND Flash。</p><p>下面来分析NOR Flash启动部分代码：</p><br /><div class="codebox"><pre class="vscroll"><code> adr r0, _start /* r0 &lt;- current position of code */
 
 ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
 
/* 判断U-Boot是否是下载到RAM中运行，若是，则不用再复制到RAM中了，这种情况通常在调试U-Boot时才发生 */
 
 cmp  r0, r1 /*_start等于_TEXT_BASE说明是下载到RAM中运行 */
 
 beq stack_setup
 
/* 以下直到nand_boot标号前都是NOR Flash启动的代码 */
 
 ldr r2, _armboot_start /*flash中armboot_start的起始地址*/
 
 ldr r3, _bss_start /*uboot_bss的起始地址*/
 
 sub r2, r3, r2 /* r2 &lt;- size of armbootuboot实际程序代码的大小 */
 
 add r2, r0, r2 /* r2 &lt;- source end address */
 
/*搬运U-Boot自身到RAM中*/
 
copy_loop:
 
 ldmia r0!, {r3-r10} /* 从地址为[r0]的NOR Flash中读入8个字的数据 */
 
 stmia r1!, {r3-r10} /* 将r3至r10寄存器的数据复制给地址为[r1]的内存 */
 
 cmp r0, r2 /* until source end addreee [r2] */
 
 ble copy_loop
 
 b stack_setup /* 跳过NAND Flash启动的代码 */</code></pre></div><p>下面再来分析NAND Flash启动部分代码：</p><br /><div class="codebox"><pre class="vscroll"><code>nand_boot:
 
 mov r1, #NAND_CTL_BASE
 
 ldr r2, =( (7&lt;&lt;12)|(7&lt;&lt;8)|(7&lt;&lt;4)|(0&lt;&lt;0) )
 str r2, [r1, #oNFCONF] /* 设置NFCONF寄存器 */
 /* 设置NFCONT，初始化ECC编/解码器，禁止NAND Flash片选 */
 ldr r2, =( (1&lt;&lt;4)|(0&lt;&lt;1)|(1&lt;&lt;0) )
 str r2, [r1, #oNFCONT]
 ldr r2, =(0x6) /* 设置NFSTAT */
 str r2, [r1, #oNFSTAT]
 /* 复位命令，第一次使用NAND Flash前复位 */
 mov r2, #0xff
 strb r2, [r1, #oNFCMD]
 mov r3, #0
 /* 为调用C函数nand_read_ll准备堆栈 */
 ldr sp, DW_STACK_START
 mov fp, #0
 /* 下面先设置r0至r2，然后调用nand_read_ll函数将U-Boot读入RAM */
 ldr r0, =TEXT_BASE /* 目的地址：U-Boot在RAM的开始地址 */
 mov r1, #0x0  /* 源地址：U-Boot在NAND Flash中的开始地址 */
 mov r2, #0x30000  /* 复制的大小，必须比u-boot.bin文件大，并且必须是NAND Flash块大小的整数倍，这里设置为0x30000（192KB） */
 bl nand_read_ll  /* 跳转到nand_read_ll函数，开始复制U-Boot到RAM */
 tst r0, #0x0 /* 检查返回值是否正确 */
 beq stack_setup
 bad_nand_read:
 loop2: b loop2 //infinite loop
.align 2
 DW_STACK_START: .word STACK_BASE+STACK_SIZE-4</code></pre></div><p>其中NAND_CTL_BASE，oNFCONF等在include/configs/mini2440.h中定义如下</p><br /><div class="codebox"><pre><code>#define NAND_CTL_BASE 0x4E000000 // NAND Flash控制寄存器基址
 
#define STACK_BASE 0x33F00000 //base address of stack
#define STACK_SIZE 0x8000 //size of stack
#define oNFCONF 0x00 /* NFCONF相对于NAND_CTL_BASE偏移地址 */
#define oNFCONT 0x04 /* NFCONT相对于NAND_CTL_BASE偏移地址*/
#define oNFADDR 0x0c /* NFADDR相对于NAND_CTL_BASE偏移地址*/
#define oNFDATA 0x10 /* NFDATA相对于NAND_CTL_BASE偏移地址*/
#define oNFCMD 0x08 /* NFCMD相对于NAND_CTL_BASE偏移地址*/
#define oNFSTAT 0x20 /* NFSTAT相对于NAND_CTL_BASE偏移地址*/
#define oNFECC 0x2c /* NFECC相对于NAND_CTL_BASE偏移地址*/</code></pre></div><p>NAND Flash各个控制寄存器的设置在S3C2440的数据手册有详细说明，这里就不介绍了。</p><p>代码中nand_read_ll函数的作用是在NAND Flash中搬运U-Boot到RAM，该函数在board/samsung/mini2440/nand_read.c中定义。</p><p>NAND Flash根据page大小可分为2种： 512B/page和2048B/page的。这两种NAND Flash的读操作是不同的。因此就需要U-Boot识别到NAND Flash的类型，然后采用相应的读操作，也就是说nand_read_ll函数要能自动适应两种NAND Flash。</p><p>参考S3C2440的数据手册可以知道：根据NFCONF寄存器的Bit3（AdvFlash (Read only)）和Bit2 （PageSize (Read only)）可以判断NAND Flash的类型。Bit2、Bit3与NAND Flash的block类型的关系如下表所示：</p><p>表 2.4 NFCONF的Bit3、Bit2与NAND Flash的关系</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/3248505820.png" alt="FluxBB bbcode 测试" /></span></p><p>由于的NAND Flash只有512B/page和2048 B/page这两种，因此根据NFCONF寄存器的Bit3即可区分这两种NAND Flash了。</p><p>完整代码见board/samsung/mini2440/nand_read.c中的nand_read_ll函数，这里给出伪代码：</p><br /><div class="codebox"><pre><code>int nand_read_ll(unsigned char *buf, unsigned long start_addr, int size)
{
	//根据NFCONF寄存器的Bit3来区分2种NAND Flash
 
	if( NFCONF &amp; 0x8 )  /* Bit是1，表示是2KB/page的NAND Flash */
	{
		
		读取2K block 的NAND Flash
		
	
	}
	else /* Bit是0，表示是512B/page的NAND Flash */
	{
	
		/
		读取512B block 的NAND Flash
		/
	
	}
 
	return 0;
}</code></pre></div><p>（10）设置堆栈</p><br /><div class="codebox"><pre><code>stack_setup:
 
 ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */
 
 sub r0, r0, #CONFIG_SYS_MALLOC_LEN /* malloc area */
 
 sub r0, r0, #CONFIG_SYS_GBL_DATA_SIZE /* 跳过全局数据区 */
 
#ifdef CONFIG_USE_IRQ
 
 sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
 
#endif
 
 sub sp, r0, #12 /* leave 3 words for abort-stack */</code></pre></div><p>只要将sp指针指向一段没有被使用的内存就完成栈的设置了。根据上面的代码可以知道U-Boot内存使用情况了，如下图所示：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2508304791.jpg" alt="FluxBB bbcode 测试" /></span></p><p>（11）清除BSS段</p><br /><div class="codebox"><pre><code>clear_bss:
 
 ldr r0, _bss_start /* BSS段开始地址，在u-boot.lds中指定*/
 
 ldr r1, _bss_end /* BSS段结束地址，在u-boot.lds中指定*/
 
 mov r2, #0x00000000
 
clbss_l:str r2, [r0] /* 将bss段清零*/
 
 add r0, r0, #4
 
 cmp  r0, r1
 
 ble clbss_l</code></pre></div><p>初始值为0，无初始值的全局变量，静态变量将自动被放在BSS段。应该将这些变量的初始值赋为0，否则这些变量的初始值将是一个随机的值，若有些程序直接使用这些没有初始化的变量将引起未知的后果。</p><p>（12）跳转到第二阶段代码入口</p><br /><div class="codebox"><pre><code>ldr   pc, _start_armboot
 
_start_armboot:   .word  start_armboot  //跳转到第二阶段代码入口start_armboot处</code></pre></div><p>UBOOT 启动第二阶段代码分析</p><p>start_armboot函数在lib_arm/board.c中定义，是U-Boot第二阶段代码的入口。U-Boot启动第二阶段流程如下：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/135465687.jpg" alt="FluxBB bbcode 测试" /></span></p><p>分析start_armboot函数前先来看看一些重要的数据结构：</p><p>（1）gd_t结构体</p><p>U-Boot使用了一个结构体gd_t来存储全局数据区的数据，这个结构体在include/asm-arm/global_data.h中定义如下：</p><br /><div class="codebox"><pre><code>typedef struct global_data {
 
 bd_t *bd;
 unsigned long flags;
 unsigned long baudrate;
 unsigned long have_console; /* serial_init() was called */
 unsigned long env_addr; /* Address of Environment struct */
 unsigned long env_valid; /* Checksum of Environment valid */
 unsigned long fb_base; /* base address of frame buffer */
 void **jt; /* jump table */
 
} gd_t;</code></pre></div><p>U-Boot使用了一个存储在寄存器中的指针gd来记录全局数据区的地址：</p><br /><div class="codebox"><pre><code>#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm (&quot;r8&quot;)</code></pre></div><p>DECLARE_GLOBAL_DATA_PTR定义一个gd_t全局数据结构的指针，这个指针存放在指定的寄存器r8中。这个声明也避免编译器把r8分配给其它的变量。任何想要访问全局数据区的代码，只要代码开头加入“DECLARE_GLOBAL_DATA_PTR”一行代码，然后就可以使用gd指针来访问全局数据区了。</p><p>根据U-Boot内存使用图中可以计算gd的值：</p><br /><div class="codebox"><pre><code>gd = TEXT_BASE - CONFIG_SYS_MALLOC_LEN - sizeof(gd_t)</code></pre></div><p>（2）bd_t结构体</p><p>bd_t在include/asm-arm.u/u-boot.h中定义如下：</p><br /><div class="codebox"><pre><code>typedef struct bd_info {
 
	int bi_baudrate;  /* 串口通讯波特率 */
	unsigned long bi_ip_addr;  /* IP 地址*/
	struct environment_s  *bi_env; /* 环境变量开始地址 */
	ulong  bi_arch_number; /* 开发板的机器码 */
	ulong  bi_boot_params; /* 内核参数的开始地址 */
 
	struct /* RAM配置信息 */
	{
		ulong start;
		ulong size;
	}bi_dram[CONFIG_NR_DRAM_BANKS];
 
} bd_t;</code></pre></div><p> U-Boot启动内核时要给内核传递参数，这时就要使用gd_t，bd_t结构体中的信息来设置标记列表。</p><p>第一阶段调用start_armboot指向C语言执行代码区，首先它要从内存上的重定位数据获得不完全配置的全局数据表格和板级信息表格，即获得gd_t和bd_t，</p><p>这两个类型变量记录了刚启动时的信息，并将要记录作为引导内核和文件系统的参数，如bootargs等等，并且将来还会在启动内核时，由uboot交由kernel时会有所用。</p><p>（3）init_sequence数组</p><p>U-Boot使用一个数组init_sequence来存储对于大多数开发板都要执行的初始化函数的函数指针。init_sequence数组中有较多的编译选项，去掉编译选项后init_sequence数组如下所示：</p><br /><div class="codebox"><pre><code>typedef int (init_fnc_t) (void);
 
init_fnc_t *init_sequence[] = {
 
 board_init,   /*开发板相关的配置--board/samsung/mini2440/mini2440.c */
 timer_init, /* 时钟初始化-- cpu/arm920t/s3c24x0/timer.c */
 env_init,  /*初始化环境变量--common/env_flash.c 或common/env_nand.c*/
 init_baudrate, /*初始化波特率-- lib_arm/board.c */
 serial_init, /* 串口初始化-- drivers/serial/serial_s3c24x0.c */
 console_init_f, /* 控制通讯台初始化阶段1-- common/console.c */
 display_banner, /*打印U-Boot版本、编译的时间-- gedit lib_arm/board.c */
 dram_init, /*配置可用的RAM-- board/samsung/mini2440/mini2440.c */
 display_dram_config, /* 显示RAM大小-- lib_arm/board.c */
 NULL,
 
};</code></pre></div><p>其中的board_init函数在board/samsung/mini2440/mini2440.c中定义，该函数设置了MPLLCOM，UPLLCON，以及一些GPIO寄存器的值，还设置了U-Boot机器码和内核启动参数地址 ：</p><br /><div class="codebox"><pre><code>/* MINI2440开发板的机器码 */
 
gd-&gt;bd-&gt;bi_arch_number = MACH_TYPE_MINI2440;
 
/* 内核启动参数地址 */
 
gd-&gt;bd-&gt;bi_boot_params = 0x30000100;  </code></pre></div><p>其中的dram_init函数在board/samsung/mini2440/mini2440.c中定义如下：</p><br /><div class="codebox"><pre><code>int dram_init (void)
{
 
 /* 由于mini2440只有 */
 
 gd-&gt;bd-&gt;bi_dram[0].start = PHYS_SDRAM_1;
 gd-&gt;bd-&gt;bi_dram[0].size = PHYS_SDRAM_1_SIZE;
 
 return 0;
}</code></pre></div><p>mini2440使用2片32MB的SDRAM组成了64MB的内存，接在存储控制器的BANK6，地址空间是0x30000000~0x34000000。</p><p>在include/configs/mini2440.h中 PHYS_SDRAM_1和PHYS_SDRAM_1_SIZE 分别被定义为0x30000000和0x04000000（64M）</p><p>分析完上述的数据结构，下面来分析start_armboot函数：</p><br /><div class="codebox"><pre class="vscroll"><code>void start_armboot (void)
{
	init_fnc_t **init_fnc_ptr;
	char *s;
	
	… …
	
	/* 计算全局数据结构的地址gd */
	gd = (gd_t*)(_armboot_start - CONFIG_SYS_MALLOC_LEN - sizeof(gd_t));
	
	… …
	
	memset ((void*)gd, 0, sizeof (gd_t));
	gd-&gt;bd = (bd_t*)((char*)gd - sizeof(bd_t));
	memset (gd-&gt;bd, 0, sizeof (bd_t));
	gd-&gt;flags |= GD_FLG_RELOC;
	
	monitor_flash_len = _bss_start - _armboot_start;
	
	/* 逐个调用init_sequence数组中的初始化函数 */
	for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
	
		if ((*init_fnc_ptr)() != 0) {
		
			hang ();
		}
	}
 
 
	/* armboot_start 在cpu/arm920t/start.S 中被初始化为u-boot.lds连接脚本中的_start */
	mem_malloc_init (_armboot_start - CONFIG_SYS_MALLOC_LEN,CONFIG_SYS_MALLOC_LEN);
 
 
 
	/* NOR Flash初始化 */
	
	#ifndef CONFIG_SYS_NO_FLASH
		/* configure available FLASH banks */
		display_flash_config (flash_init ());
	#endif /* CONFIG_SYS_NO_FLASH */
 
	… …
	
	/* NAND Flash 初始化*/
	
	#if defined(CONFIG_CMD_NAND)
		puts (&quot;NAND: &quot;);
		nand_init(); /* go init the NAND */
	#endif
 
	… …
	
	/*配置环境变量，重新定位 */
	env_relocate ();
	
	… …
 
	/* 从环境变量中获取IP地址 */
	gd-&gt;bd-&gt;bi_ip_addr = getenv_IPaddr (&quot;ipaddr&quot;);
	stdio_init (); /* get the devices list going. */
	jumptable_init ();
	
	… …
	
	/* fully init console as a device */
	console_init_r (); 
	
	… …
	
	/* enable exceptions */
	enable_interrupts ();
 
  
  // USB 初始化
	#ifdef CONFIG_USB_DEVICE
		usb_init_slave();
	#endif
 
	/* Initialize from environment */
	
	if ((s = getenv (&quot;loadaddr&quot;)) != NULL) {
		load_addr = simple_strtoul (s, NULL, 16);
	}
 
	#if defined(CONFIG_CMD_NET)
	
	if ((s = getenv (&quot;bootfile&quot;)) != NULL) {
		copy_filename (BootFile, s, sizeof (BootFile));
	}
	
	#endif
 
	… …
	
	/* 网卡初始化 */
	
	#if defined(CONFIG_CMD_NET)
	
	#if defined(CONFIG_NET_MULTI)
	
		puts (&quot;Net: &quot;);
	
	#endif
	
	eth_initialize(gd-&gt;bd);
 
	… …
 
	#endif
 
 
	/* main_loop() can return to retry autoboot, if so just run it again. */
	
	for (;;) {
	
		main_loop ();
	
	}
 
	/* NOTREACHED - no way out of command loop except booting */
 
}</code></pre></div><p>main_loop函数在common/main.c中定义。一般情况下，进入main_loop函数若干秒内没有按键触发就进入kernel 执行流程</p><p>UBOOT启动Linux过程</p><p>U-Boot使用标记列表（tagged list）的方式向Linux传递参数。标记的数据结构式是tag，在U-Boot源代码目录include/asm-arm/setup.h中定义如下：</p><br /><div class="codebox"><pre class="vscroll"><code>struct tag_header {
 
	u32 size; /* 表示tag数据结构的联合u实质存放的数据的大小*/
	u32 tag;  /* 表示标记的类型 */
 
};
 
struct tag {
 
	struct tag_header hdr;
	
	union {
	
		struct tag_core core;
		struct tag_mem32 mem;
		struct tag_videotext videotext;
		struct tag_ramdisk ramdisk;
		struct tag_initrd initrd;
		struct tag_serialnr serialnr;
		struct tag_revision revision;
		struct tag_videolfb videolfb;
		struct tag_cmdline cmdline;
		
		 /*
		 * Acorn specific
		 */
		
		 struct tag_acorn acorn;
		
		 /*
		 * DC21285 specific
		 */
		
		 struct tag_memclk memclk;
	
	 } u;
 
};</code></pre></div><p>U-Boot使用命令bootm来启动已经加载到内存中的内核。而bootm命令实际上调用的是do_bootm函数。对于Linux内核，do_bootm函数会调用do_bootm_linux函数来设置标记列表和启动内核。do_bootm_linux函数在lib_arm/bootm.c 中定义如下：</p><br /><div class="codebox"><pre class="vscroll"><code>int do_bootm_linux(int flag, int argc, char *argv[], bootm_headers_t *images)
{
 
	bd_t *bd = gd-&gt;bd;
	char *s;
	int machid = bd-&gt;bi_arch_number;
	void (*theKernel)(int zero, int arch, uint params);
 
#ifdef CONFIG_CMDLINE_TAG
 
	char *commandline = getenv (&quot;bootargs&quot;); /* U-Boot环境变量bootargs */
 
#endif
 
	…
	
	theKernel = (void (*)(int, int, uint))images-&gt;ep; /* 获取内核入口地址 */
	
	…
 
	#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
	
	defined (CONFIG_CMDLINE_TAG) || \
	
	defined (CONFIG_INITRD_TAG) || \
	
	defined (CONFIG_SERIAL_TAG) || \
	
	defined (CONFIG_REVISION_TAG) || \
	
	defined (CONFIG_LCD) || \
	
	defined (CONFIG_VFD)
	
	setup_start_tag (bd); /* 设置ATAG_CORE标志 */
	
	…
	
	#ifdef CONFIG_SETUP_MEMORY_TAGS
		setup_memory_tags (bd);  /* 设置内存标记 */
	#endif
	
	#ifdef CONFIG_CMDLINE_TAG
		setup_commandline_tag (bd, commandline); /* 设置命令行标记 */
	#endif
	
	…
	
	setup_end_tag (bd); /* 设置ATAG_NONE标志 */
	
	#endif
	
	
	/* we assume that the kernel is in place */
	
	printf (&quot;\nStarting kernel ...\n\n&quot;);
	
	…
	
	cleanup_before_linux (); /* 启动内核前对CPU作最后的设置 */
	
	theKernel (0, machid, bd-&gt;bi_boot_params); /* 调用内核 */
	
	/* does not return */
	
	return 1;
 
}</code></pre></div><p>其中的setup_start_tag，setup_memory_tags，setup_end_tag函数在lib_arm/bootm.c中定义如下：</p><p>（1）setup_start_tag函数</p><br /><div class="codebox"><pre><code>static void setup_start_tag (bd_t *bd)
{
	params = (struct tag *) bd-&gt;bi_boot_params; /* 内核的参数的开始地址 */
	
	params-&gt;hdr.tag = ATAG_CORE;
	params-&gt;hdr.size = tag_size (tag_core);
	params-&gt;u.core.flags = 0;
	params-&gt;u.core.pagesize = 0;
	params-&gt;u.core.rootdev = 0;
	params = tag_next (params);
}</code></pre></div><p>标记列表必须以ATAG_CORE开始，setup_start_tag函数在内核的参数的开始地址设置了一个ATAG_CORE标记</p><p>（2）setup_memory_tags函数</p><br /><div class="codebox"><pre><code>static void setup_memory_tags (bd_t *bd)
{
	int i;
 
	/*设置一个内存标记 */
 
	for (i = 0; i &lt; CONFIG_NR_DRAM_BANKS; i++) {
 
		params-&gt;hdr.tag = ATAG_MEM;
		params-&gt;hdr.size = tag_size (tag_mem32);
		params-&gt;u.mem.start = bd-&gt;bi_dram[i].start;
		params-&gt;u.mem.size = bd-&gt;bi_dram[i].size;
		params = tag_next (params);
 
	}
}</code></pre></div><p>setup_memory_tags函数设置了一个ATAG_MEM标记，该标记包含内存起始地址，内存大小这两个参数。</p><p>（3）setup_end_tag函数</p><br /><div class="codebox"><pre><code>static void setup_end_tag (bd_t *bd)
{
	params-&gt;hdr.tag = ATAG_NONE;
	params-&gt;hdr.size = 0;
}</code></pre></div><p>标记列表必须以标记ATAG_NONE结束，setup_end_tag函数设置了一个ATAG_NONE标记，表示标记列表的结束。</p><p>U-Boot设置好标记列表后就要调用内核了。但调用内核前，CPU必须满足下面的条件：</p><p>（1） CPU寄存器的设置<br />&gt;Ø&#160; r0=0<br />&gt;Ø&#160; r1=机器码<br />&gt;Ø&#160; r2=内核参数标记列表在RAM中的起始地址</p><p>（2）CPU工作模式<br />&gt;Ø&#160; 禁止IRQ与FIQ中断<br />&gt;Ø&#160; CPU为SVC模式</p><p>（3） 使数据Cache与指令Cache失效</p><p>do_bootm_linux中调用的cleanup_before_linux函数完成了禁止中断和使Cache失效的功能。cleanup_before_linux函数在cpu/arm920t/cpu.中定义：</p><br /><div class="codebox"><pre><code>int cleanup_before_linux (void)
{
	/*
	* this function is called just before we call linux
	* it prepares the processor for linux
	*
	* we turn off caches etc ...
	*/
	
	disable_interrupts (); /* 禁止FIQ/IRQ中断 */
	
	/* turn off I/D-cache */
	
	icache_disable(); /* 使指令Cache失效 */
	
	dcache_disable(); /* 使数据Cache失效 */
	
	/* flush I/D-cache */
	
	cache_flush(); /* 刷新Cache */
	
	return 0;
}</code></pre></div><p>由于U-Boot启动以来就一直工作在SVC模式，因此CPU的工作模式就无需设置了。</p><br /><div class="codebox"><pre><code>do_bootm_linux中:
 
void (*theKernel)(int zero, int arch, uint params);
 
… …
 
theKernel = (void (*)(int, int, uint))images-&gt;ep;
 
… …
 
theKernel (0, machid, bd-&gt;bi_boot_params);</code></pre></div><p>第73行代码将内核的入口地址“images-&gt;ep”强制类型转换为函数指针。根据ATPCS规则，函数的参数个数不超过4个时，使用r0~r3这4个寄存器来传递参数。因此第128行的函数调用则会将0放入r0，机器码machid放入r1，内核参数地址bd-&gt;bi_boot_params放入r2，从而完成了寄存器的设置，最后转到内核的入口地址。</p><p>到这里，U-Boot的工作就结束了，系统跳转到Linux内核代码执行。</p><p>UBOOT 添加命令的方法及U-Boot命令执行过程</p><p>下面以添加menu命令（启动菜单）为例讲解U-Boot添加命令的方法。</p><p>（1）建立common/cmd_menu.c</p><p>习惯上通用命令源代码放在common目录下，与开发板专有命令源代码则放在board/&lt;board_dir&gt;目录下，并且习惯以“cmd_&lt;命令名&gt;.c”为文件名。</p><p>（2）定义“menu”命令</p><p>在cmd_menu.c中使用如下的代码定义“menu”命令：</p><br /><div class="codebox"><pre><code>_BOOT_CMD(
 
       menu,    3,    0,    do_menu,
       &quot;menu - display a menu, to select the items to do something\n&quot;,
       &quot; - display a menu, to select the items to do something&quot;
 
);</code></pre></div><p>其中U_BOOT_CMD命令格式如下：</p><p>U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) 各个参数的意义如下：<br />&gt;name：命令名，非字符串，但在U_BOOT_CMD中用“#”符号转化为字符串<br />&gt;maxargs：命令的最大参数个数<br />&gt;rep：是否自动重复（按Enter键是否会重复执行）<br />&gt;cmd：该命令对应的响应函数<br />&gt;usage：简短的使用说明（字符串）<br />&gt;help：较详细的使用说明（字符串）</p><p>在内存中保存命令的help字段会占用一定的内存，通过配置U-Boot可以选择是否保存help字段。若在include/configs/mini2440.h中定义了CONFIG_SYS_LONGHELP宏，则在U-Boot中使用help命令查看某个命令的帮助信息时将显示usage和help字段的内容，否则就只显示usage字段的内容。</p><p>U_BOOT_CMD宏在include/command.h中定义：</p><br /><div class="codebox"><pre><code>#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
 
 “##”与“#”都是预编译操作符，“##”有字符串连接的功能，“#”表示后面紧接着的是一个字符串。
 
//其中的cmd_tbl_t在include/command.h中定义如下：
 
struct cmd_tbl_s {
 
 char *name; /* 命令名 */
 int maxargs; /* 最大参数个数 */
 int repeatable; /* 是否自动重复 */
 int (*cmd)(struct cmd_tbl_s *, int, int, char *[]); /* 响应函数 */
 char *usage; /* 简短的帮助信息 */
 
#ifdef CONFIG_SYS_LONGHELP
 char *help; /* 较详细的帮助信息 */
#endif
 
#ifdef CONFIG_AUTO_COMPLETE
 
 /* 自动补全参数 */
 int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
 
#endif
 
};</code></pre></div><div class="codebox"><pre><code>typedef struct cmd_tbl_s  cmd_tbl_t;
 
一个cmd_tbl_t结构体变量包含了调用一条命令的所需要的信息。
 
其中Struct_Section在include/command.h中定义如下：
 
#define Struct_Section  __attribute__ ((unused,section (&quot;.u_boot_cmd&quot;)))
 
凡是带有__attribute__ ((unused,section (&quot;.u_boot_cmd&quot;))属性声明的变量都将被存放在&quot;.u_boot_cmd&quot;段中，
并且即使该变量没有在代码中显式的使用编译器也不产生警告信息。
 
在U-Boot连接脚本u-boot.lds中定义了&quot;.u_boot_cmd&quot;段：
 
  . = .;
  __u_boot_cmd_start = .;          /*将 __u_boot_cmd_start指定为当前地址 */
  .u_boot_cmd : { *(.u_boot_cmd) }
  __u_boot_cmd_end = .;           /*  将__u_boot_cmd_end指定为当前地址  */</code></pre></div><p>这表明带有“.u_boot_cmd”声明的函数或变量将存储在“u_boot_cmd”段。这样只要将U-Boot所有命令对应的cmd_tbl_t变量加上“.u_boot_cmd”声明，编译器就会自动将其放在“u_boot_cmd”段，查找cmd_tbl_t变量时只要在__u_boot_cmd_start与__u_boot_cmd_end之间查找就可以了。</p><p>因此“menu”命令的定义经过宏展开后如下：</p><p>cmd_tbl_t __u_boot_cmd_menu __attribute__ ((unused,section (&quot;.u_boot_cmd&quot;))) = {menu, 3, 0, do_menu, &quot;menu - display a menu, to select the items to do something\n&quot;, &quot; - display a menu, to select the items to do something&quot;}</p><p>实质上就是用U_BOOT_CMD宏定义的信息构造了一个cmd_tbl_t类型的结构体。编译器将该结构体放在“u_boot_cmd”段，执行命令时就可以在“u_boot_cmd”段查找到对应的 cmd_tbl_t类型结构体。</p><p>（3）实现命令的函数</p><p>在cmd_menu.c中添加“menu”命令的响应函数的实现。具体的实现代码略：</p><br /><div class="codebox"><pre><code>int do_menu (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
    /* 实现代码略 */
}</code></pre></div><p>（4）将common/cmd_menu.c编译进u-boot.bin</p><p>在common/Makefile中加入如下代码：</p><div class="codebox"><pre><code>COBJS-$(CONFIG_BOOT_MENU) += cmd_menu.o</code></pre></div><p>在include/configs/mini2440.h加入如代码：</p><div class="codebox"><pre><code>#define CONFIG_BOOT_MENU 1</code></pre></div><p>重新编译下载U-Boot就可以使用menu命令了</p><p>（5）menu命令执行的过程</p><p>在U-Boot中输入“menu”命令执行时，U-Boot接收输入的字符串“menu”，传递给run_command函数。run_command函数调用common/command.c中实现的find_cmd函数在__u_boot_cmd_start与__u_boot_cmd_end间查找命令，并返回menu命令的cmd_tbl_t结构。然后run_command函数使用返回的cmd_tbl_t结构中的函数指针调用menu命令的响应函数do_menu，从而完成了命令的执行。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 03 Apr 2024 14:04:17 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=855&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo 之 Initial RAM filesystem and RAM disk]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=854&amp;action=new</link>
			<description><![CDATA[<p>一、简介</p><p>(1) initrd</p><p>在早期的linux系统中，一般只有硬盘或者软盘被用来作为linux根文件系统的存储设备，因此也就很容易把这些设备的驱动程序集成到内核中。但是现在的嵌入式系统中可能将根文件系统保存到各种存储设备上，包括scsi、sata，u-disk等等。因此把这些设备的驱动代码全部编译到内核中显然就不是很方便。</p><p>&#160; 为了解决这一矛盾，于是出现了基于ramdisk的initrd( bootloader initialized RAM disk )。Initrd是一个被压缩过的小型根目录，这个目录中包含了启动阶段中必须的驱动模块，可执行文件和启动脚本。当系统启动的时候，bootloader会把initrd文件读到内存中，然后把initrd文件在内存中的起始地址和大小传递给内核。内核在启动初始化过程中会解压缩initrd文件，然后将解压后的initrd挂载为根目录，然后执行根目录中的/linuxrc脚本（cpio格式的initrd为/init,而image格式的initrd&lt;也称老式块设备的initrd或传统的文件镜像格式的initrd&gt;为/initrc），您就可以在这个脚本中加载realfs（真实文件系统）存放设备的驱动程序以及在/dev目录下建立必要的设备节点。这样，就可以mount真正的根目录，并切换到这个根目录中来。</p><p>(2) Initramfs</p><p>在linux2.5中出现了initramfs，它的作用和initrd类似，只是和内核编译成一个文件(该initramfs是经过gzip压缩后的cpio格式的数据文件)，该cpio格式的文件被链接进了内核中特殊的数据段.init.ramfs上，其中全局变量__initramfs_start和__initramfs_end分别指向这个数据段的起始地址和结束地址。内核启动时会对.init.ramfs段中的数据进行解压，然后使用它作为临时的根文件系统。</p><p>二、initramfs与initrd区别</p><p>(1) Linux内核只认cpio格式的initramfs文件包(因为unpack_to_rootfs只能解析cpio格式文件)，非cpio格式的 initramfs文件包将被系统抛弃，而initrd可以是cpio包也可以是传统的镜像(image)文件，实际使用中initrd都是传统镜像文件。<br />(2) initramfs在编译内核的同时被编译并与内核连接成一个文件，它被链接到地址__initramfs_start处，与内核同时被 bootloader加载到ram中，而initrd是另外单独编译生成的，是一个独立的文件，它由bootloader单独加载到ram中内核空间外的地址，比如加载的地址为addr(是物理地址而非虚拟地址)，大小为8MB，那么只要在命令行加入&quot;initrd=addr,8M&quot;命令，系统就可以找到 initrd(当然通过适当修改Linux的目录结构，makefile文件和相关代码，以上两种情况都是可以相通的)。<br />(3) initramfs被解析处理后原始的cpio包(压缩或非压缩)所占的空间(&amp;__initramfs_start - &amp;__initramfs_end)是作为系统的一部分直接保留在系统中，不会被释放掉，而对于initrd镜像文件，如果没有在命令行中设置&quot;keepinitd&quot;命令，那么initrd镜像文件被处理后其原始文件所占的空间(initrd_end - initrd_start)将被释放掉。<br />(4) initramfs可以独立ram disk单独存在，而要支持initrd必须要先支持ram disk，即要配置CONFIG_BLK_DEV_INITRD选项 -- 支持initrd，必须先要配置CONFIG_BLK_DEV_RAM -- 支持ram disk ，因为initrd image实际就是初始化好了的ramdisk镜像文件，最后都要解析、写入到ram disk设备/dev/ram或/dev/ram0中。注: 使用initramfs，命令行参数将不需要&quot;initrd=&quot;和&quot;root=&quot;命令<br />initramfs利弊:<br />------------------------------------------------------<br />由于initramfs使用cpio包格式，所以很容易将一个单一的文件、目录、node编译链接到系统中去，这样很简单的系统中使用起来很方便，不需要另外挂接文件系统。<br />但是因为cpio包实际是文件、目录、节点的描述语言包，为了描述一个文件、目录、节点，要增加很多额外的描述文字开销，特别是对于目录和节点，本身很小额外添加的描述文字却很多，这样使得cpio包比相应的image文件大很多。</p><br /><p>使用initramfs的内核配置(使用initramfs做根文件系统):<br />------------------------------------------------------<br />General setup&#160; ---&gt;<br />[ ] Initial RAM filesystem and RAM disk (initramfs/initrd) support<br />(/rootfs_dir) Initramfs source file(s)&#160; &#160;//输入根文件系统的所在目录 <br />使用initramfs的内核启动参数不需要&quot;initrd=&quot;和&quot;root=&quot;参数,但是必须在initramfs中创建/init文件或者修改内核启动最后代码(init文件是软连接，指向什么? init -&gt; bin/busybox，否则内核启动将会失败)<br />链接入内核的initramfs文件在linux-2.6.24/usr/initramfs_data.cpio.gz<br />使用initrd的内核配置(使用网口将根文件系统下载到RAM -- tftp addr ramdisk.gz):<br />------------------------------------------------------<br />1. 配置initrd<br />General setup&#160; ---&gt;<br />[ ] Initial RAM filesystem and RAM disk (initramfs/initrd) support<br />() Initramfs source file(s)&#160; &#160;//清空根文件系统的目录配置 <br />2. 配置ramdisk<br />Device Drivers&#160; ---&gt;&#160; &#160;<br />Block devices&#160; ---&gt;<br />&lt; &gt; RAM disk support<br />(16)&#160; Default number of RAM disks&#160; &#160;// 内核在/dev/目录下生成16个ram设备节点<br />(4096) Default RAM disk size (kbytes)<br />(1024) Default RAM disk block size (bytes)<br />使用 initrd的内 核启动参数:initrd=addr,0x400000 root=/dev/ram rw<br />注:<br />(1) addr是根文件系统的下载地址；<br />(2) 0x400000是根文件系统的大小，该大小需要和内核配置的ramdisk size 4096 kbytes相一致；<br />(3) /dev/ram是ramdisk的设备节点，rw表示根文件系统可读、可写；<br />根文件系统存放在FLASH分区：<br />------------------------------------------------------<br />1. 内核启动参数不需要&quot;initrd=&quot;(也可以写成&quot;noinitrd&quot;)；<br />root=/dev/mtdblock2 (/dev/mtdblock2 -- 根文件系统所烧写的FLASH分区)<br />2. 内核配置不需要ram disk；也不需要配置initramfs或者initrd<br />[ ] Initial RAM filesystem and RAM disk (initramfs/initrd) support <br />注: boot的FLASH分区要和kernel的FLASH分区匹配(而非一致)，需要进一步解释。</p><br /><p>处理流程<br />linux内核支持两种格式的文件系统镜像：传统格式的文件系统镜像image-initrd和cpio-initrd格式的镜像。<br />下面分别说明：</p><p>cpio-initrd的处理流程：（执行流程可以对照下面博文的代码分析：linux的initrd机制和initramfs机制之根文件挂载流程：代码分析）<br />1．uboot把内核以及initrd文件加载到内存的特定位置。<br />2．内核判断initrd的文件格式，如果是cpio格式。<br />3．将initrd的内容释放到rootfs中。<br />4．执行initrd中的/init文件，执行到这一点，内核的工作全部结束，完全交给/init文件处理。<br />可见对于cpio-initrd格式的镜像，它执行的是init文件</p><p>image-initrd的处理流程<br />1．uboot把内核以及initrd文件加载到内存的特定位置。<br />2．内核判断initrd的文件格式，如果不是cpio格式，将其作为image-initrd处理。<br />3．内核将initrd的内容保存在rootfs下的/initrd.image文件中。<br />4．内核将/initrd.image的内容读入/dev/ram0设备中，也就是读入了一个内存盘中。<br />5．接着内核以可读写的方式把/dev/ram0设备挂载为原始的根文件系统。<br />6．如果/dev/ram0被指定为真正的根文件系统，那么内核跳至最后一步正常启动。<br />7．执行initrd上的/linuxrc文件，linuxrc通常是一个脚本文件，负责加载内核访问根文件系统必须的驱动，以及加载根文件系统。<br />8．/linuxrc执行完毕，实际根文件系统被挂载，执行权转交给内核。<br />9．如果实际根文件系统存在/initrd目录，那么/dev/ram0将从/移动到/initrd。否则如果/initrd目录不存在，/dev/ram0将被卸载。<br />10．在实际根文件系统上进行正常启动过程，执行/sbin/init。<br />对于image-initrd格式的镜像，它执行的是linuxrc文件</p><p>三、两种格式镜像比较<br />1. cpio-initrd的制作方法比image-initrd简单。<br />2. cpio-initrd的内核处理流程相比image-initrd更简单，因为：<br />a. 根据上面的流程对比可知，cpio-initrd格式的镜像是释放到rootfs中的，不需要额外的文件系统支持，<br />&#160; &#160;而image-initrd格式的镜像先是被挂载成虚拟文件系统，而后被卸载，基于具体的文件系统<br />b. image-initrd内核在执行完/linuxrc进程后，还要返回执行内核进行一些收尾工作，<br />&#160; &#160;并且要负责执行真正的根文件系统的/sbin/init。</p><p>处理流程对比如下图所示：（来自网络）<br /><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/4263602854.jpg" alt="FluxBB bbcode 测试" /></span></p><p>由对比可以看出cpio-initrd格式的镜像更具优势，这也是它逐渐代替image-initrd格式镜像的原因</p><p>四、initrd镜像的制作</p><p>cpio-initrd格式镜像制作：<br />进入到要制作的文件系统的根目录；</p><div class="codebox"><pre><code>bash# find . | cpio -c -o &gt; ../initrd.img
bash# gzip ../initrd.img</code></pre></div><p>image-initrd格式镜像制作：<br />进入到要制作的文件系统的根目录；</p><div class="codebox"><pre><code>bash# dd if=/dev/zero of=../initrd.img bs=512k count=5
bash# mkfs.ext2 -F -m0 ../initrd.img
bash# mount -t ext2 -o loop ../initrd.img /mnt
bash# cp -r * /mnt
bash# umount /mnt
bash# gzip -9 ../initrd.img</code></pre></div><p>对于image-initrd格式镜像的制作，往往采用制作工具，如genext2fs</p><p>五、image-initrd格式镜像实例解读<br />参见下一篇博文<br />一、initrd</p><p>ram disk中的file system叫做initrd，全名叫做initial ramdisk。<br />如何创建initial ramisk</p><div class="codebox"><pre><code>host &gt; dd if=/dev/zero of=/dev/ram0 bs=1k count=&lt;count&gt;
host &gt; mke2fs -vm0 /dev/ram0 &lt;count&gt;
host &gt; tune2fs -c 0 /dev/ram0
host &gt; dd if=/dev/ram0 bs=1k count=&lt;count&gt; | gzip -v9 &gt; ramdisk.gz</code></pre></div><p>这段代码就创建了大小为count的ramdisk<br />创建完之后还要添加哪些东西</p><p>还要添加一些必要的文件让他工作，可能是库，应用程序等。例如busybox。</p><div class="codebox"><pre><code>host $ mkdir mnt
host $ gunzip ramdisk.gz
host $ mount -o loop ramdisk mnt/
host $ ... copy stuff you want to have in ramdisk to mnt...
host $ umount mnt
host $ gzip -v9 ramdisk</code></pre></div><p>内核如何支持initial ramdisk</p><div class="codebox"><pre><code>#
# General setup
#
...
CONFIG_BLK_DEV_INITRD=y
CONFIG_INITRAMFS_SOURCE=&quot;&quot;
...

#
# UBI - Unsorted block images
#
.../*****************initramfs 应该不需要配置下面的参数************************/
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_COUNT=1
CONFIG_BLK_DEV_RAM_SIZE=8192
CONFIG_BLK_DEV_RAM_BLOCKSIZE=1024</code></pre></div><p>告诉uboot怎么找到她</p><div class="codebox"><pre><code>UBOOT # tftp 0x87000000 ramdisk.gz
UBOOT # erase 0x2200000 +0x&lt;filesize&gt;
UBOOT # cp.b 0x87000000 0x2200000 0x&lt;filesize&gt;

UBOOT # setenv bootargs ... root=/dev/ram0 rw initrd=0x87000000,8M
UBOOT # setenv bootcmd cp.b 0x2200000 0x87000000 0x&lt;filesize&gt;; bootm
UBOOT # saveenv</code></pre></div><p>注意： ramdisk 中要有ram0节点<br />最后启动内核<br />二、initramfs</p><p>initramfs相当于把initrd放进了内核，通过cpio（这是一个文件处理工具）实现。<br />如何创建<br />比initrd简单多了</p><div class="codebox"><pre><code>host &gt; mkdir target_fs

host &gt; ... copy stuff you want to have in initramfs to target_fs...</code></pre></div><p>注意：<br />1. initramfs中的cpio系统不能处理hard link，用soft link<br />2. 顶层必须有个init程序，这是kernel要用的，可以这么做</p><div class="codebox"><pre><code>/init -&gt; /bin/busybox</code></pre></div><p>接着</p><div class="codebox"><pre><code>host &gt; cd target_fs
host &gt; find . | cpio -H newc -o &gt; ../target_fs.cpio</code></pre></div><p>内核支持</p><div class="codebox"><pre><code>#
# General setup
#
...
CONFIG_BLK_DEV_INITRD=y
CONFIG_INITRAMFS_SOURCE=&quot;&lt;path_to&gt;/target_fs&gt;&quot;
...

#
# UBI - Unsorted block images
#
...
CONFIG_BLK_DEV_RAM=y
CONFIG_BLK_DEV_RAM_COUNT=1
CONFIG_BLK_DEV_RAM_SIZE=8192
CONFIG_BLK_DEV_RAM_BLOCKSIZE=1024
 </code></pre></div><p>然后执行make uImage的时候就被包含到kernel中了。<br />uboot支持</p><p>因为已经在kernel中了，不需要像initrd一样通过参数 root=/xxx rw initrd=xxx来告诉uboot了</p><p>三、比较<br />&#160; &#160; initrd方式中kernel和initial file system为独立的部分，互不影响，下载的时候镜像也小。<br />&#160; &#160; 创建修改initramfs比initrd容易。<br />&#160; &#160; 在烧写的时候，显然一个镜像更容易管理。</p><p>一、简介</p><p>(1) initrd</p><p>&#160; 在早期的linux系统中，一般只有硬盘或者软盘被用来作为linux根文件系统的存储设备，因此也就很容易把这些设备的驱动程序集成到内核中。但是现在的嵌入式系统中可能将根文件系统保存到各种存储设备上，包括scsi、sata，u-disk等等。因此把这些设备的驱动代码全部编译到内核中显然就不是很方便。</p><p>&#160; 为了解决这一矛盾，于是出现了基于ramdisk的initrd( bootloader initialized RAM disk )。Initrd是一个被压缩过的小型根目录，这个目录中包含了启动阶段中必须的驱动模块，可执行文件和启动脚本。当系统启动的时候，bootloader会把initrd文件读到内存中，然后把initrd文件在内存中的起始地址和大小传递给内核。内核在启动初始化过程中会解压缩initrd文件，然后将解压后的initrd挂载为根目录，然后执行根目录中的/linuxrc脚本（cpio格式的initrd为/init,而image格式的initrd&lt;也称老式块设备的initrd或传统的文件镜像格式的initrd&gt;为/initrc），您就可以在这个脚本中加载realfs（真实文件系统）存放设备的驱动程序以及在/dev目录下建立必要的设备节点。这样，就可以mount真正的根目录，并切换到这个根目录中来。</p><p>(2) Initramfs</p><p>&#160; 在linux2.5中出现了initramfs，它的作用和initrd类似，只是和内核编译成一个文件(该initramfs是经过gzip压缩后的cpio格式的数据文件)，该cpio格式的文件被链接进了内核中特殊的数据段.init.ramfs上，其中全局变量__initramfs_start和__initramfs_end分别指向这个数据段的起始地址和结束地址。内核启动时会对.init.ramfs段中的数据进行解压，然后使用它作为临时的根文件系统。</p><p>二、initramfs与initrd区别</p><br /><p>(1) Linux内核只认cpio格式的initramfs文件包(因为unpack_to_rootfs只能解析cpio格式文件)，非cpio格式的 initramfs文件包将被系统抛弃，而initrd可以是cpio包也可以是传统的镜像(image)文件，实际使用中initrd都是传统镜像文件。<br />(2) initramfs在编译内核的同时被编译并与内核连接成一个文件，它被链接到地址__initramfs_start处，与内核同时被 bootloader加载到ram中，而initrd是另外单独编译生成的，是一个独立的文件，它由bootloader单独加载到ram中内核空间外的地址，比如加载的地址为addr(是物理地址而非虚拟地址)，大小为8MB，那么只要在命令行加入&quot;initrd=addr,8M&quot;命令，系统就可以找到 initrd(当然通过适当修改Linux的目录结构，makefile文件和相关代码，以上两种情况都是可以相通的)。<br />(3) initramfs被解析处理后原始的cpio包(压缩或非压缩)所占的空间(&amp;__initramfs_start - &amp;__initramfs_end)是作为系统的一部分直接保留在系统中，不会被释放掉，而对于initrd镜像文件，如果没有在命令行中设置&quot;keepinitd&quot;命令，那么initrd镜像文件被处理后其原始文件所占的空间(initrd_end - initrd_start)将被释放掉。<br />(4) initramfs可以独立ram disk单独存在，而要支持initrd必须要先支持ram disk，即要配置CONFIG_BLK_DEV_INITRD选项 -- 支持initrd，必须先要配置CONFIG_BLK_DEV_RAM -- 支持ram disk ，因为initrd image实际就是初始化好了的ramdisk镜像文件，最后都要解析、写入到ram disk设备/dev/ram或/dev/ram0中。注: 使用initramfs，命令行参数将不需要&quot;initrd=&quot;和&quot;root=&quot;命令<br />initramfs利弊:<br />------------------------------------------------------<br />由于initramfs使用cpio包格式，所以很容易将一个单一的文件、目录、node编译链接到系统中去，这样很简单的系统中使用起来很方便，不需要另外挂接文件系统。<br />但是因为cpio包实际是文件、目录、节点的描述语言包，为了描述一个文件、目录、节点，要增加很多额外的描述文字开销，特别是对于目录和节点，本身很小额外添加的描述文字却很多，这样使得cpio包比相应的image文件大很多。</p><br /><p>使用initramfs的内核配置(使用initramfs做根文件系统):<br />------------------------------------------------------<br />General setup&#160; ---&gt;<br />[ ] Initial RAM filesystem and RAM disk (initramfs/initrd) support<br />(/rootfs_dir) Initramfs source file(s)&#160; &#160;//输入根文件系统的所在目录 <br />使用initramfs的内核启动参数不需要&quot;initrd=&quot;和&quot;root=&quot;参数,但是必须在initramfs中创建/init文件或者修改内核启动最后代码(init文件是软连接，指向什么? init -&gt; bin/busybox，否则内核启动将会失败)<br />链接入内核的initramfs文件在linux-2.6.24/usr/initramfs_data.cpio.gz<br />使用initrd的内核配置(使用网口将根文件系统下载到RAM -- tftp addr ramdisk.gz):<br />------------------------------------------------------<br />1. 配置initrd<br />General setup&#160; ---&gt;<br />[ ] Initial RAM filesystem and RAM disk (initramfs/initrd) support<br />() Initramfs source file(s)&#160; &#160;//清空根文件系统的目录配置 <br />2. 配置ramdisk<br />Device Drivers&#160; ---&gt;&#160; &#160;<br />Block devices&#160; ---&gt;<br />&lt; &gt; RAM disk support<br />(16)&#160; Default number of RAM disks&#160; &#160;// 内核在/dev/目录下生成16个ram设备节点<br />(4096) Default RAM disk size (kbytes)<br />(1024) Default RAM disk block size (bytes)<br />使用 initrd的内 核启动参数:initrd=addr,0x400000 root=/dev/ram rw<br />注:<br />(1) addr是根文件系统的下载地址；<br />(2) 0x400000是根文件系统的大小，该大小需要和内核配置的ramdisk size 4096 kbytes相一致；<br />(3) /dev/ram是ramdisk的设备节点，rw表示根文件系统可读、可写；<br />根文件系统存放在FLASH分区：<br />------------------------------------------------------<br />1. 内核启动参数不需要&quot;initrd=&quot;(也可以写成&quot;noinitrd&quot;)；<br />root=/dev/mtdblock2 (/dev/mtdblock2 -- 根文件系统所烧写的FLASH分区)<br />2. 内核配置不需要ram disk；也不需要配置initramfs或者initrd<br />[ ] Initial RAM filesystem and RAM disk (initramfs/initrd) support <br />注: boot的FLASH分区要和kernel的FLASH分区匹配(而非一致)，需要进一步解释。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 02 Apr 2024 12:54:48 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=854&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[x86架构：从实模式进入保护模式]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=483&amp;action=new</link>
			<description><![CDATA[<p>一、先聊聊保护模式的基本概念：为什么叫保护模式？到底保护了啥？是怎么保护的？</p><p> 1、保护模式：要搞定出保护模式的意义，需要理解另一个概念：实模式；实模式下，存在以下重大的安全隐患：</p><p>&#160; &#160; &#160;（1）内存没权限区别：用户程序可以访问任意内存，包括操作系统运行的内存、其他程序运行的内存（个人观点：这是万恶之源。病毒、木马、外挂都是想尽各种办法访问操作系统或其他进程的代码/数据，达到自己的某些特定目的），所有的代码和数据都是能查询和删改的，毫无隐私可言</p><p>&#160; &#160; &#160;（2）用户程序可以随意访问和删除段寄存器，达到和上述同样的效果</p><p>&#160; &#160; &#160;（3）对I/O端口无限制的访问，比如打开文件、发送网络数据包、打印到屏幕或分配内存等；</p><p>&#160; &#160; &#160;（4）可以执行所有指令</p><p>&#160; 2、保护模式保护了啥？ 本质上：</p><p>&#160; &#160; &#160;（1）限制应用程序对内存、寄存器的访问，保护操作系统、其他进程的代码/数据不被恶意访问、甚至删改！</p><p>&#160; &#160; &#160;（2）限制用户程序对某些I/O端口的访问。</p><p>&#160; &#160; &#160;（3）限制用户程序执行某些指令</p><p>&#160; 3、怎么保护的？</p><p>&#160; &#160; &#160; &#160;（1）通过GDT/LDT/IDT表、CS/DS/SS等段寄存器，严格限制用户程序对内存的读写：访问内存前，CS中的CPL先和selector选择子中的RPL对比，数值小的再和GDT表的DPL比，如果数值小于等于，说明是有权限的，才能继续访问对应的内存！（这里多说几句: 64位的windows已经平坦了，进程访问的虚拟地址已经拉通，段寄存器名存实亡；进程之间隔离主要通过CR3，把不同进程同样的虚拟地址映射到不同的物理地址；换句话说，内存保护主要靠页表、MMU等隔离不同进程的物理内存）<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202102/2052730-20210225113629065-1941658048.png" alt="FluxBB bbcode 测试" /></span> </p><p>附： <br />&#160; CPL：全称current privilege level，存放在代码段寄存器中（cs），代表当前执行程序的特权级；常见的调试器都能查到：一般都是11，即所谓的3环！<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202102/2052730-20210225121121570-1315433629.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; RPL:&#160; 全称request privilege level，请求特权级，存放在段选择子中。注意，不是段寄存器中，不是段寄存器中，不是段寄存器中；它的含义是当前我想以 RPL 这个级别来请求你把这个段选择子置入段寄存器。实际上 RPL 并没有什么用（个人观点，仅供参考），因为请求者任何时刻都可以让 RPL = 0。但是如果请求者是 CPL = 0 的程序，用 RPL = 3 的级别来请求 DPL = 0的数据段，必然会失败；<br />&#160; DPL:&#160; 全称descriptor privilege level，存放在段描述符中，用于表示段的特权级</p><p>&#160; &#160; &#160; &#160;（2）要想访问必须通过操作系统内核，比如打开文件、发送网络数据包、打印到屏幕或分配内存</p><p>&#160; &#160; &#160;</p><p>二、这里简化一下说说要点：</p><p>1、 生成并加载GDT表</p><p>&#160; 实模式下任何进程可以无限制读写任何内存，甚至os的内存，毫无安全性可言；需要对用户进程读写内存的地址做严格限制，衍生出了保护模式；保护模式将内存分成不同的段，段基址、limit、各种属性存放在GDT表；用户程序读写段内存时需要先通过段寄存器的selector在GDT找到段描述符，查看是否有权限、偏移地址是否超过limit等。如果一切ok，可以继续读写段内数据；</p><p>&#160; cs、ds、ss段的描述符：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200604151403191-277785221.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; 注意点：（1）用户程序运行在3环，是没有权限更改GDT的，所以这种方式完全可以限制3环程序对内存的读写；windows要想改GDT，要么连接windbg，要么写驱动；</p><p>&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; （2）lgdt [cs:gdt_size]，从操作数指向的地址取6字节，高4字节作为gdt的基址，低2字节作为gdt中描述符的个数，如下：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200604151519462-892260349.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160; 这6字节早在编译时就确定了：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200604151610240-1896131387.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; &#160;（3）GDT的机制是CPU定的，操作系统负责运维和使用各段的数据 </p><p> </p><p>2、打开A20</p><p>&#160; &#160; &#160;8086下，地址线有20位，寻址范围从0x00000~0xfffff，超过0xfffff的地址会被cpu重新从0x00000开始，相当于丢掉进位（或把地址对0xfffff取模）。80286以后，地址总线扩展到24位，为了访问0x100000~0x10FFEF之间的内存，而不是象8086/8088那样回绕到0，需要开启A20地址线，方法如下：</p><div class="codebox"><pre><code>   mov dx,0x92                        ;南桥ICH芯片内的端口0x92
    in al,dx
    or al,0x02
    out dx,al                        ;打开A20</code></pre></div><p>3、开启保护模式</p><p>&#160; &#160; &#160;CPU提供的控制寄存器CR0~CR3用于控制CPU的运行模式。CR0第一位（0位）是保护模式允许位（Protection Enable，PE），如果把这个位置为1，那么处理器将会进入保护模式，核心代码如下：</p><div class="codebox"><pre><code>    cli                                ;关闭中断，但后面一直没打开
    
    mov eax,cr0
    or eax,0x01
    mov cr0,eax                        ;设置PE位，处理器进入保护模式</code></pre></div><br /><br /><p>&#160; &#160; 注意：要先调用cli把if置0，关闭中断，避免给CR0赋值时被打断；</p><p> </p><p>4、清空段寄存器中的缓存和旧流水线指令</p><p> （1）段寄存器实际上有96位，保护模式下的汇编指令只能操作位于低16位的selector，剩余80位中部分作为缓存（可用sreg命令查看段寄存器dh、dl的值，这些都是描述符的缓存），存储了段基址。只要段不改变，缓存就不会更新；但保护模式下不能直接用实模式的地址（权限不够、位数不对等），需要清空；</p><p>&#160; &#160;（2）cpu和内存的速度差异很大，为了提高效率，cpu会提前预测并执行某些分支指令（幽灵漏洞就是这么来的，细节可参考B站的一个科普视频：https://www.bilibili.com/video/BV1eW411i7ZM?from=search&amp;seid=2138940898008952062），这就是所谓的乱序执行，进入保护模式后这些指令的逻辑结果可能是有问题的，也要马上清除；</p><p>&#160; &#160; &#160;清除各个段寄存器、乱序指令缓存的办法：马上调用jmp指令跳转，让cpu认为当前各种缓存已经失效，核心代码如下：</p><div class="codebox"><pre><code>;保护模式
    jmp 0x0008:flush-$$                ;现在是在16位保护模式下，0x0008依然是段的选择子，而flush则是偏移地址
    [bits 32]
flush:                                ;头部加了vstart=0x7c00,这里变成了0x7c85，所以上面的jmp中的偏移要减去开头的基址，得到偏移    
    mov cx,0x0010                    
    mov ds,cx</code></pre></div><p>5、完整代码：</p><div class="codebox"><pre class="vscroll"><code>;---------------------保护模式主引导扇区程序---------------------
SECTION protectModel vstart=0x7c00 align=16    
    mov ax,0x00 
    mov ss,ax
    mov sp,0x7c00
    
    mov ax,[cs:gdt_base];ax=0x7e00
    mov dx,[cs:gdt_base+0x02];dx=0x0000;
    ;mov ax,[cs:gdt_base+0x7c00];这段已经被加载到0x7c00，所以需要加上；ax=0x7e00
    ;mov dx,[cs:gdt_base+0x7c00+0x02];dx=0x0000;
    mov bx,0x10
    div bx
    
    mov ds,ax                        ;得到base基地址，ds=0x7e0
    mov bx,dx                        ;得到偏移地址，这里是0x0000

;---------------------安装描述符---------------------
    ;描述符0
    mov dword [ebx+0x00],0x00        ;第一个描述符必须是0；这里默认是ds段，也就是gdt_base的基址
    mov dword [ebx+0x04],0x00        ;ebx是gdt表的偏移，ds是gdt的基址
    
    ;描述符1
    mov dword [ebx+0x08],0x7c0001FF
    mov dword [ebx+0x0c],0x00409800    ;基地址0x00007c00,段界限0x001FF,粒度是字节，
                                    ;长度是512字节，在内存中的32位段，特权级为0，只能执行的代码段
    ;描述符2
    mov dword [ebx+0x10],0x8000FFFF    
    mov dword [ebx+0x14],0x0040920B ;基地址0x000B8000,段界限0x0FFFF,粒度是字节，
                                    ;长度是64KB，在内存中的32位段，特权级为0，可以读写的向上拓展的数据段
    ;描述符3                                
    mov dword [ebx+0x18],0x00007A00    
    mov dword [ebx+0x1c],0x00409600 ;基地址0x00000000,段界限0x07A00,粒度是字节，
                                    ;在内存中的32位段，特权级为0，可以读写的向下拓展的栈段
                                    
    mov word [cs:gdt_size],31;写入GDT段界限，4个描述符是32个字节，所以界限就是31
    lgdt [cs:gdt_size]        ;load gdt
    ;mov word [cs:gdt_size+0x7c00],31;写入GDT段界限，4个描述符是32个字节，所以界限就是31
    ;lgdt [cs:gdt_size+0x7c00]        ;load gdt
    
    mov dx,0x92                        ;南桥ICH芯片内的端口0x92
    in al,dx
    or al,0x02
    out dx,al                        ;打开A20
    
    cli                                ;关闭中断，但后面一直没打开
    
    mov eax,cr0
    or eax,0x01
    mov cr0,eax                        ;设置PE位，处理器进入保护模式
    
    ;保护模式
    jmp 0x0008:flush-$$                ;现在是在16位保护模式下，0x0008依然是段的选择子，而flush则是偏移地址
    [bits 32]
flush:                                ;头部加了vstart=0x7c00,这里变成了0x7c85，所以上面的jmp中的偏移要减去开头的基址，得到偏移    
    mov cx,0x0010                    
    mov ds,cx
    
    ;以下在屏幕上显示&quot;Protect mode OK.&quot; 
    mov byte [0x00],&#039;P&#039;  
    mov byte [0x02],&#039;r&#039;
    mov byte [0x04],&#039;o&#039;
    mov byte [0x06],&#039;t&#039;
    mov byte [0x08],&#039;e&#039;
    mov byte [0x0a],&#039;c&#039;
    mov byte [0x0c],&#039;t&#039;
    mov byte [0x0e],&#039; &#039;
    mov byte [0x10],&#039;m&#039;
    mov byte [0x12],&#039;o&#039;
    mov byte [0x14],&#039;d&#039;
    mov byte [0x16],&#039;e&#039;
    mov byte [0x18],&#039; &#039;
    mov byte [0x1a],&#039;O&#039;
    mov byte [0x1c],&#039;K&#039;
         
    ;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作 
     mov cx,00000000000_11_000B         ;加载堆栈段选择子
     mov ss,cx
     mov esp,0x7c00

     mov ebp,esp                        ;保存堆栈指针 
     push byte &#039;.&#039;                      ;压入立即数（字节）
     
     sub ebp,4
     cmp ebp,esp                        ;判断压入立即数时，ESP是否减4 
     jnz ghalt                          
     pop eax
     mov [0x1e],al                      ;显示句点 
      
  ghalt:     
         hlt                                ;已经禁止中断，将不会被唤醒 

;-------------------------------------------------------------------------------
     
    gdt_size    dw 0
    gdt_base    dd 0x00007e00        ;GDT的物理地址,主引导扇区是512个字节，这个地址刚好在主引导扇区之后 
                             
    times 510-($-$$) db 0
                     db 0x55,0xaa</code></pre></div><p>说明：</p><p>（1）整段代码被加载到0x7c00处，所以在开头额外加vstart=0x7c00，让nasm从这个地址开始编译；</p><p>&#160; &#160; 紧接着这4行代码也要更改：去掉0x7c00，因为gdt_base和gdt_size已经相对0x7c00计算偏移</p><p>&#160; mov ax,[cs:gdt_base+0x7c00]<br />&#160; &#160;mov dx,[cs:gdt_base+0x7c00+0x02]</p><p>&#160; mov word [cs:gdt_size+0x7c00],31;写入GDT段界限，4个描述符是32个字节，所以界限就是31<br />&#160; &#160;lgdt [cs:gdt_size+0x7c00]&#160; &#160; &#160;</p><p>（2）flush也是相对0x7c00开始计算偏移的，具体偏移是0x7c85，远超代码段的limit，导致出错异常，又跳回biso启动处执行<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200604145925462-1701187500.png" alt="FluxBB bbcode 测试" /></span> </p><p>解决办法也简单，直接改成jmp 0x0008:flush-$$ 即可，让后面的偏移值相对于段开始的地方，这次对了，如下：<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200604150350218-1371873647.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; （3）81行代码：push byte &#039;.&#039;&#160; ，但nasm还是编译成push 0x0000002e, 估计是为了内存对齐<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200604151231152-500169496.png" alt="FluxBB bbcode 测试" /></span> </p><p>&#160; &#160; （4）效果：在频幕上打印一行字<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200604151751530-1737906203.png" alt="FluxBB bbcode 测试" /></span> </p><p> 参考：</p><p>1、https://manybutfinite.com/post/cpu-rings-privilege-and-protection/&#160; cpu运行级别和保护机制</p><p>2、 <a href="https://www.cnblogs.com/Philip-Tell-Truth/p/5211248.html" rel="nofollow">https://www.cnblogs.com/Philip-Tell-Tru … 11248.html</a>&#160; &#160;</p><p>3、x86汇编：从实模式到保护模式</p><p>4、 <a href="https://blog.csdn.net/q1007729991/article/details/52727332" rel="nofollow">https://blog.csdn.net/q1007729991/artic … s/52727332</a>&#160; cpu特权等级</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 12 Oct 2022 05:00:02 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=483&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[x86架构： 硬件启动过程分析（附引导启动代码）]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=482&amp;action=new</link>
			<description><![CDATA[<p>用户按下开机键，几秒的时间，都经历了啥？</p><p>1、cpu各个寄存器赋初始值，cs.base=0xffff0000, eip=0xfff0，其他寄存器都是0，这时cs:ip得到的物理地址：0xfffffff0;</p><p>&#160; &#160; &#160;cpu上电后为啥会把cs:ip赋成这种初始值了？ 可能是希望把BIOS-ROM放在可寻址4GB最高端，给操作系统和用户程序大段完整的RAM空间，便于后者在运行时的内存管理<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200607114438577-1961598627.png" alt="FluxBB bbcode 测试" /></span></p><p>2、cpu跳转到0xffff0执行。但由于该地址距离0xfffff（实模式下内存空间只有1M）仅16byte，空间十分有限，无法执行复杂逻辑，只能jmp到其0xf000:e05b继续执行；<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200602124205120-1474881594.png" alt="FluxBB bbcode 测试" /></span></p><p>3、0xf000:e05b任然是BIOS的地址，继续执行检测代码，看看内存（RAM）、显示器、键盘、鼠标、硬盘等外设是否完好。如有问题，会发出长短不等的滴滴声响，可凭此判断故障类型<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200602124654540-986451939.png" alt="FluxBB bbcode 测试" /></span></p><p> 4、外设检测完，如果一切正常，会查找用户设置的启动顺序。普通用户首次安装OS时一般选择从CD/DVD启动，装OS；装好后取出光盘，BIOS会自动从磁盘加载MBR到0x7c00；<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200602124939352-1544171368.png" alt="FluxBB bbcode 测试" /></span></p><p> 5、加载MBR到0x7c00后，jmp到这里继续执行；由于只加载一个扇区，能执行的代码不超过510字节（还有2字节是扇区结尾的标识：0xaa55），能干的事也有限，所以MBR一般会继续从磁盘其他地方把os代码都拷贝到内存，同时重定位代码，完成os的加载；</p><p>6、继续jmp到os代码执行；</p><p>这6步中，1-4部不用我们操心，厂家的产品在出厂前已经做好；第5部，从磁盘的0柱面、0磁头、1扇区加载MBR到内存0x7c00处也BIOS干的，不需要开发人员操心；真正需要开发人员编写代码的地方：</p><p>&#160; &#160; MBR的代码，这部分代码被bios加载到内存后需要做什么？<br />&#160; &#160; MBR代码能运行的代码不超过510字节，真正的os肯定不止这点代码，剩余代码怎么办？</p><p>既然MBR能运行的代码不超过510字节，能干的活有限，那么干脆简单点，把os或用户程序剩余代码加载到内存，完成重定位，再跳到这些代码执行，具体代码如下:</p><p>1、MBR代码</p><div class="codebox"><pre class="vscroll"><code>;MBR 主引导扇区
;开机上电后，BIOS会自动从0x7c00处执行

    lba_num equ 100;一共101个扇区，用户程序在硬盘中的逻辑扇区号
    
SECTION mbr vstart=0x7c00 align=16;以16位对齐
                                  ;cs和IP已经运行到这里，不用再设置了
        mov ax,0
        mov ss,ax;堆栈段从0开始
        mov sp,ax;
        
        mov ax,[cs:phy_address];目前在cs段，如果不写，默认读ds段；
        mov dx,[cs:phy_address+0x2]
        mov bx,0x10
        div bx;相当于右移4bit，得到0x1000，就是段地址，放在ax
        mov ds,ax; 
        xor bx,bx;  
        
        mov si,lba_num
        xor di,di
        call read_disk;先读第一个扇区，把用户程序的头部加载到内存，才能得到重定位表
        
        mov ax,[0];program_len分别放在ax和bx;
        mov dx,[2];从内存读数据，不加段前缀的默认是ds；
        mov bx,512
        div bx;ax = 用户程序的扇区个数  dx=扇区余数，也就是最后不满一个扇区内偏移
        cmp dx,0; test dx,dx
        jnz cantDiv;不能被整除，说明有数据不满一个扇区的数据，但也要占用一个扇区的空间
        dec ax;扇区数减一：前面已经读了一个扇区。
        
        
cantDiv:
        cmp ax,0;已经读完了，可以直接重定位
        jz  realloc;
        mov cx,ax;剩余扇区数放入cx，方便后续loop
        push ds
        
Continue_Read:
        inc si
        mov ax,ds
        add ax,0x20;基址增加0x20，相当于增加512byte，比如：ds:bx = 0000:0000 = 00000; ds:bx = 0020:0000 = 0200+0000=0x0200=512byte 
        mov ds,ax;往高地址挪一个扇区512byte
        xor bx,bx;偏移清零，通过段基址挪动
        call read_disk;相当于寄存器传参
        loop Continue_Read
        pop ds


;---------------上面都是把数据从磁盘读到内存，下面开始重定位------------------------------------
;先计算出用户程序code_entry在内存的绝对地址
realloc:
        mov ax,[0x06];默认是ds段，此时已是0x1000；code_entry的section.code1.start低2字节
        mov dx,[0x08];code_entry的section.code1.start高2字节
        call reallocaddress
        mov [0x06],ax;把内存中的物理地址写回去，这次得到绝对物理地址了；
        ;mov [0x06],ds
        mov cx,[0x0a];5个段需要重定位
        mov bx,0x0c;
        
;用户程序每个section都计算出内存的绝对地址，然后写回去        
reallocLoop:
        mov ax,[bx]
        mov dx,[bx+2]
        call reallocaddress
        mov [bx],ax;
        add bx,4
        loop reallocLoop
        
        jmp far [0x04];内存操作默认以ds基址，这里是0x10000；跳转到用户程序start变量地址
        ;mov ax, [0x04];得到offset，就是start的偏移地址
        ;jmp 0x1000:ax

;dx:ax 32位偏移地址，寄存器传参
;输出16位段基址,保存在ax
reallocaddress:
        push dx
        add ax,[cs:phy_address];注意：目前在cs段，不加从内存读数据默认用ds，此处为用户程序;ax=0x0000+[0x10006]=0x0020;
        add dx,[cs:phy_address+0x2];dx=0x0001
        shr ax,4;低16位地址的低4位去掉，高4位补零，得到段基址;ax=0x0002
        ror dx,4;高16位地址循环右移；dx=0x1000
        and dx,0xf000;取出最需要的4bit，其他清零；dx=0x1000
        or ax,dx;ax=0x1002
        pop dx 
        ret


;ds:bx 从硬盘读数据到该物理地址
;di, si 是逻辑扇区号：逻辑扇区只用28位，所以di有4位是不用的；si是逻辑扇区低16位
;可以通过int 0x13中断读取，也可以通过磁盘控制器读取；
read_disk:
        push ax
        push bx
        push cx
        push dx
        push si
        push di

        ;https://www.cnblogs.com/mlzrq/p/10223060.html 详细说明
        mov dx,0x1f2;磁盘端口,指定读取或写入的扇区数
        mov al,1;每次读一个扇区
        out dx,al;往端口写入数据
        
        inc dx;0x1f3  lba地址的低8位，就是0-7位
        mov ax,si;
        out dx,al;先把低8位写入端口，因为用户程序被写入了磁盘100号扇区，所以调用函数传参数di=100
        
        inc dx;0x1f4  lba地址的中8位，就是8-15位
        mov al,ah
        out dx,al;
        
        inc dx;0x1f5  lba地址的高8位，就是16-23位
        mov ax,di;
        out dx,al;
        
        ;上面3个已经把前面24位填满，这里填最高4位
        inc dx;0x1f6  lba地址的前4位，就是24-27位
        mov al,0xe0; 高4位是各种标志位： 0 CHS,1 LBA; 1; 0 从  1 主; 0； 这里是e；
        or al,ah
        out dx,al

        inc dx;0x1f7
        mov al,0x20;发送读扇区的请求：0x20
        out dx,al
        
;------------------------------    and al,0x88 逻辑上出错，先屏蔽试试    
waits:
        in al,dx; 从0x1f7读取磁盘状态，一共有8位；第7位：1表示busy   第3位：1表示准备好读写操作，所以在0xxx1xxx的时候才能读写，其他状态都不行；
        and al,0x88;第7位和第3位保持不变，其他清零
        cmp al,0x08;
        jnz waits;状态不等于0x08，说明没准备好，继续等待
        
        
        mov dx,0x01f0;数据端口,16位，需要ax接数据；每个扇区512byte，每次读2byte，要读256次
        mov cx,256;
        
        ;准备好了，开始读磁盘
readw:
        in ax,dx;
        mov [bx],ax;
        add bx,2;每次读2byte
        loop readw;

        pop di
        pop si
        pop dx
        pop cx
        pop bx
        pop ax

        ret
        
        


        phy_address          dd 0x10000;用户程序拷贝到内存地址
                 
        times    510 - ($-$$) db 0; 
                             dw 0xaa55</code></pre></div><p>2、用户程序</p><div class="codebox"><pre class="vscroll"><code>;用户程序
;段的数目并未限制，用户可根据需求自行创建

;-------------------------------------------------------------------------------
SECTION header vstart=0;vstart=0连着写，不能有空格
        program_len                dd    program_end; 
        code_entry                dw    start;变量偏移0x4；
                                dd    section.code1.start;code1段基址：变量偏移0x6
                                
        reallocate_item            dw    (header_end-code1Segment)/4 ;每个段偏移都是dd=4byte,变量偏移0xa
        
        ;重定位表，记录重要段相对于程序起始位置的偏移
        code1Segment            dd  section.code1.start;变量偏移0xc
        data1Segment            dd  section.data1.start;变量偏移0x10  本section在文件中的真实偏移量（真实地址）,或则说相对开始的偏移地址
        stack1Segment            dd  section.stack1.start;变量偏移0x14
        use1Segment            dd  section.use1.start;变量偏移0x18
        use1DataSegment        dd  section.use1Data.start;变量偏移0x1c
header_end:   ;有vstart = 0，header_end从vstart = 0开始算偏移


;-------------------------------------------------------------------------------
SECTION use1 align=16 vstart=0; 


;-------------------------------------------------------------------------------
SECTION use1Data align=16 vstart=0; 


use1Data_end:
;-------------------------------------------------------------------------------
SECTION code1 align=16 vstart=0; ;vstart=0连这些，不能有空格
;直接调用BIOS例程在显示器打印
start:
    
    mov ax,[stack1Segment];初始化堆栈
    mov ss,ax
    mov ax,stacker_pointer;
    mov sp,ax;

    xor ah,ah
    mov al,0x03
    int 0x10;调用bios的0x10号中断清屏
    
    ;AL=写模式，BH=页码，BL=颜色，CX=字符串长度，DH=行，DL=列，ES:BP=字符串偏移量
    ;https://zh.wikipedia.org/wiki/INT_10H 有详细说明
    mov ah,0x13
    mov al,1
    xor bh,bh
    mov bl,0x04
    mov cx, data1_end - msg;cx保存字符串长度
    mov dh,12;显示的行号
    mov dl,25;显示的列号
    mov bp,msg; es:bp指向需要打印的字符串
    push ax
    mov ax,[data1Segment]
    ;mov ax,cs;
    mov es,ax;es:bp 为串首地址
    pop ax
    int 0x10
    
    hlt;程序待机

;-------------------------------------------------------------------------------
SECTION data1 align=16 vstart=0

        msg db &#039;are you ready?&#039;, 0

data1_end:
;-------------------------------------------------------------------------------
SECTION stack1 align=16 vstart=0;

        resb 256; reserve byte，保留/分配256byte空间
stacker_pointer: ;栈底放在高地址
;-------------------------------------------------------------------------------
SECTION tail align=16; 这个段没有vstart = 0,那就从开头计算偏移，也就是SECTION header开始算；

program_end:</code></pre></div><p>说明: (1）SECTION用于定于段，没有数量限制，开发人员可根据需求取舍</p><p>&#160; &#160; &#160; &#160;（2）vstart=0表示该段内的标识都从0开始计算偏移。如果没有 vstart=0，那么段内标识比如msg、start等都从程序开始处计算偏移；</p><p>&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; vstart=0千万要紧挨着，不能有个空格，不能有个空格，不能有个空格，重要的事情说三遍。否则这种声明无效，段内标识的偏移还是会从程序开头处计算，导致后续逻辑出错</p><p>&#160; &#160; &#160; （3）段的数量没限制，但是建议把代码段和数据段分开，各种变量尽量在数据段声明；代码段声明的变量因未隔离开，容易被cpu当成代码执行，导致异常或逻辑错乱</p><p>&#160; &#160; &#160; （4）MBR为什么要用0xaa55了？ 0xaa55=0b 1010 1010&#160; 0101&#160; 0101，看出来有啥特点了么？&#160; 0和1交叉呈现，就像梳子一样；奇偶校验总是为偶数，a和5与是0，或是f；</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 12 Oct 2022 04:50:54 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=482&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[引导扇区]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=411&amp;action=new</link>
			<description><![CDATA[<p>我们以硬盘的引导扇区为例，0柱面0磁道1扇区是整个硬盘的第一个扇区，也是所谓的引导扇区MBR。MBR总共512字节， 446开机管理程序 boot loader，64字节的分区 表, 2字节 0x55aa校验字节。这个开机管理程序是由操作系统写进去的，所以开机管理程序认识操作系统安装分区的文件系统，可以找到操作系统的核心文件，然后加载进 内存，并 将CPU控制权交给操作系统。一个硬盘只有一个MBR,但是当将磁盘进行磁盘分区后，每个分区都会有一个启动扇区，每个分区都会被格式化为一种文件系统</p><p>启动扇区都可以安装boot loader，boot loader认识核心文件在文件系统的位置。这就是为什么可以创建双系统的原因。</p><p>首先安装windows操作系统，windows占有第一个分区叫做C盘吧，windows系统将自己的boot loader写入MBR，同时也会写入C盘分区的启动扇区(左下角的蓝块)，这时如 果有其他分区比如D,E，其他分区的启动扇区都会写入boot loader</p><p>加入你现在想安装双系统，你需要分割出一个分区给linux, 叫D分区吧(当然一旦格式化成linux的文件系统比如ext3，windows就不认识这个分区了，在windows的文档管理 里也就看不见这个盘了)，格式化分区(也就是写入文件系统信息)，然后将linux的boot loader写入MBR和自己所在分区D的启动扇区。</p><p>启动机器时执行MBR的boot loader(因为这是最后linux写入的，而linux的boot loader不会覆盖windows所在分区的启动扇区)提供选单，MBR 的 boot loader可以直接加载l inux的核心文件(因为它认识linux的文件系统), 可以转向windows启动扇区的boot loader然后再由windows 自己的boot loader加载windows的核心文件，也可以转向linux启动 扇区的boot loader然后加载Linux的核心文件。这就是多重操作系统的原理。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 30 Aug 2022 11:28:53 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=411&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[开机启动流程:]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=410&amp;action=new</link>
			<description><![CDATA[<p>打开电源， 一通电源其实CPU已经开始工作了，CS： eip 指向 FFFF:0, 而FFFF:0处存放着一条跳转指令， 所以CPU就执行这条跳转指令</p><p>&#160; &#160; 跳转指令跳转到BIOS ROM中的程序中执行，控制权交给BIOS</p><p>&#160; &#160; BIOS 进行硬件检测和一些程序初始化比如设置一些BIOS中断向量(16位下)</p><p>&#160; &#160; BIOS根据硬件设置，取得第一个可开机装置(这也是我们经常装系统是需要去BIOS中设置的开机启动装置顺序)</p><p>&#160; &#160; BIOS去开机装置中加载数据，比如硬盘或者软盘就会去读取0号柱面0号磁柱1号扇区（MBR）中的数据，加载到内存0:7c00中（同时用0x55aa作为对引导扇区的校验， 如果不是以这两个字节数据结束，数据加载失败），加载完后cs:eip指向 0:7c00（加载过程是通过调用BIOS提供的扇区读取中断程序）</p><p>&#160; &#160; 从0:7c00开始执行程序，CPU控制权从BIOS转移到开机管理程序也就是从硬盘或者软盘第一个扇区加载进来的数据，所以严格意义上说第一个程序只是一个开机管理程 序</p><p>&#160; &#160; 我们的程序调用BIOS提供的中断程序向显存中写入字符串，并传递数量，现实属性等参数</p><p>&#160; &#160; 最后调用 jmp $进入无限循环</p><p>通过这个启动流程发现其实我们写org 7c00h并不能认为将程序加载到7c00处去执行，上面7)之前全部都是硬件执行，我们控制不了，我们做的只是将数据放到确定的地 方，让硬件去读去执行，我们写org 0x7c00h 只是让编译器将其他标签的偏移地址设置正确，如果我们写成org 0x1000h，程序依然会被加载到 0:7c00处，但是 mov ax, BootMessage这句就会执行出错， 因为此时BootMessage会变成 1000h+ 1c(BootMessage和程序开头之间所有指令长度总和),但字符串实际是存在 0:7c1c处的。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 30 Aug 2022 11:25:03 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=410&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[硬盘BPB信息]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=306&amp;action=new</link>
			<description><![CDATA[<p>BS_OEMName&#160; &#160; DB &#039;ForrestY&#039;&#160; &#160; ; OEM String, 必须 8 个字节<br />&#160; &#160; BPB_BytsPerSec&#160; &#160; DW 512&#160; &#160; &#160; &#160; ; 每扇区字节数<br />&#160; &#160; BPB_SecPerClus&#160; &#160; DB 1&#160; &#160; &#160; &#160; ; 每簇多少扇区<br />&#160; &#160; BPB_RsvdSecCnt&#160; &#160; DW 1&#160; &#160; &#160; &#160; ; Boot 记录占用多少扇区<br />&#160; &#160; BPB_NumFATs&#160; &#160; DB 2&#160; &#160; &#160; &#160; ; 共有多少 FAT 表<br />&#160; &#160; BPB_RootEntCnt&#160; &#160; DW 224&#160; &#160; &#160; &#160; ; 根目录文件数最大值<br />&#160; &#160; BPB_TotSec16&#160; &#160; DW 2880&#160; &#160; &#160; &#160; ; 逻辑扇区总数<br />&#160; &#160; BPB_Media&#160; &#160; DB 0xF0&#160; &#160; &#160; &#160; ; 媒体描述符<br />&#160; &#160; BPB_FATSz16&#160; &#160; DW 9&#160; &#160; &#160; &#160; ; 每FAT扇区数<br />&#160; &#160; BPB_SecPerTrk&#160; &#160; DW 18&#160; &#160; &#160; &#160; ; 每磁道扇区数<br />&#160; &#160; BPB_NumHeads&#160; &#160; DW 2&#160; &#160; &#160; &#160; ; 磁头数(面数)<br />&#160; &#160; BPB_HiddSec&#160; &#160; DD 0&#160; &#160; &#160; &#160; ; 隐藏扇区数<br />&#160; &#160; BPB_TotSec32&#160; &#160; DD 0&#160; &#160; &#160; &#160; ; wTotalSectorCount为0时这个值记录扇区数<br />&#160; &#160; BS_DrvNum&#160; &#160; DB 0&#160; &#160; &#160; &#160; ; 中断 13 的驱动器号<br />&#160; &#160; BS_Reserved1&#160; &#160; DB 0&#160; &#160; &#160; &#160; ; 未使用<br />&#160; &#160; BS_BootSig&#160; &#160; DB 29h&#160; &#160; &#160; &#160; ; 扩展引导标记 (29h)<br />&#160; &#160; BS_VolID&#160; &#160; DD 0&#160; &#160; &#160; &#160; ; 卷序列号<br />&#160; &#160; BS_VolLab&#160; &#160; DB &#039;OrangeS0.02&#039;; 卷标, 必须 11 个字节<br />&#160; &#160; BS_FileSysType&#160; &#160; DB &#039;FAT12&#160; &#160;&#039;&#160; &#160; ; 文件系统类型, 必须 8个字节&#160; </p><br /><p>读软盘 中断13h<br />------------------------------------------------------------------------------------------<br />中断号&#160; &#160; &#160; &#160; |&#160; &#160; &#160; &#160; &#160; &#160; 寄存器&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;|&#160; 作用&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;|<br />------------------------------------------------------------------------------------------<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; |ah=00h&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; dl=驱动器号(0表示A盘)&#160; &#160; &#160; |&#160; 复位软驱&#160; &#160; &#160; &#160; &#160; &#160;|<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; |---------------------------------------------------------------------------<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; |ah=02h&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; al=要读取的扇区数&#160; &#160; &#160; &#160; &#160; |从磁盘将数据读&#160; &#160; &#160; &#160;|<br />13h&#160; &#160; &#160; &#160; &#160; &#160;|ch=柱面(磁道)号&#160; &#160; &#160; &#160; &#160; &#160;cl=起始扇区号&#160; &#160; &#160; &#160; &#160; &#160; &#160; |入es:bx指向的&#160; &#160; &#160; &#160; |<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; |dh=磁头号&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;dl=驱动器号(0表示A盘)&#160; &#160; &#160; |缓冲区&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;|<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; |es:bx-&gt;数据缓冲区&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;|&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;|<br />-----------------------------------------------------------------------------------------</p><br /><br /><br /><br /><p>BPB参数<br />&#160; &#160; &#160; 好了，接下来重点来了。首先，最开始的3各字节的数据分别是跳转指令与空指令，因为在汇编当中0xEB是跳转指令，0x58是跳转的地址，而0x90则是空指令。至于为什么要在这里放上一句跳转指令呢，这个还得从启动区开始讲起，为了节约篇幅，我就简单介绍一下：一般第一个扇区叫做启动区，cpu把扇区当中的数据当作指令来执行，当读取到EB 58 这个指令时，遍跳转到0x58这个地址并继续读取指令来执行，而0x58地址之后的内容通常都是载入操作系统的指令。如果希望知道详细内容的读者不妨去看一下《30天自制操作系统》这本书，第一天结尾部分有很详细的说明。总之这边的话FAT32规定这个3各字节的内容必须是EB 58 90，只要记住就行了（笑）。(如1L所说，EB 58 90 对应汇编代码即为JUMP 0x58; NOP;)。</p><p>&#160; &#160; &#160; &#160;而从0x03~0x0A这8个字节的数据表示OEM，这里即为“MSDOS5.0”。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:05:34 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=306&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[文件内容查找办法]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=305&amp;action=new</link>
			<description><![CDATA[<p>文件起始地址 = (保留扇区数 + FAT表扇区数 * FAT表个数(2) + (文件起始簇号-2)*每簇扇区数)*每扇区字节数</p><br /><br /><br /><br /><p>文件起始地址 =(数据区开始扇区数+文件起始簇号-2)*每扇区字节数</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:05:14 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=305&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[makefile语法]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=304&amp;action=new</link>
			<description><![CDATA[<p>Makefile里主要包含了五个东西：显式规则、隐晦规则、变量定义、文件指示和注释<br />&#160; &#160; &#160; &#160; 显式规则。显式规则说明了，如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出，要生成的文件，文件的依赖文件，生成的命令。<br />&#160; &#160; &#160; &#160; 隐晦规则。由于我们的make有自动推导的功能，所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile，这是由make所支持的。<br />&#160; &#160; &#160; &#160; 变量的定义。在Makefile中我们要定义一系列的变量，变量一般都是字符串，这个有点你C语言中的宏，当Makefile被执行时，其中的变量都会被扩展到相应的引用位置上。<br />&#160; &#160; &#160; &#160; 文件指示。其包括了三个部分，一个是在一个Makefile中引用另一个Makefile，就像C语言中的include一样；另一个是指根据某些情况指定Makefile中的有效部分，就像C语言中的预编译#if一样；还有就是定义一个多行的命令。有关这一部分的内容，我会在后续的部分中讲述。<br />&#160; &#160; &#160; &#160; &#160;注释。Makefile中只有行注释，和UNIX的Shell脚本一样，其注释是用“#”字符，这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符，可以用反斜框进行转义，如：“\#”。<br />最后，还值得一提的是，在Makefile中的命令，必须要以[Tab]键开始。</p><br /><br /><p>= 赋值，可以引用后面变量 例子：<br />foo = $(bar)<br />bar = $(ugh)<br />ugh = Huh?<br />all:<br />echo $(foo)<br />我们执行“make all”将会打出变量$(foo)的值是“Huh?”（ $(foo)的值是$(bar)，$(bar)的值是$(ugh)，$(ugh)的值是“Huh?”）可见，变量是可以使用后面的变量来定义的。</p><p>:= 赋值，只能引用前面变量</p><p>?= 例子：<br />FOO ?= bar<br />其含义是，如果FOO没有被定义过，那么变量FOO的值就是“bar”，如果FOO先前被定义过，那么这条语将什么也不做，其等价于：</p><br /><p>三、变量高级用法：</p><p>第一种是变量值的替换。<br />示例：<br />foo := a.o b.o c.o<br />bar := $(foo:.o=.c)<br />这个示例中，我们先定义了一个“$(foo)”变量，而第二行的意思是把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”，所以我们的“$(bar)”的值就是“a.c b.c c.c”。<br />另外一种变量替换的技术是以“静态模式”（参见前面章节）定义的，如：<br />oo := a.o b.o c.o<br />bar := $(foo:%.o=%.c)<br />这依赖于被替换字串中的有相同的模式，模式中必须包含一个“%”字符，这个例子同样让$(bar)变量的值为“a.c b.c c.c”。</p><p>第二种高级用法是——“把变量的值再当成变量”<br />x = y<br />y = z<br />a := $($(x))<br />在这个例子中，$(x)的值是“y”，所以$($(x))就是$(y)，于是$(a)的值就是“z”。（注意，是“x=y”，而不是“x=$(y)”）</p><br /><p>四、追加变量值：<br />+=如：<br />objects = main.o foo.o bar.o utils.o<br />objects += another.o<br />于是，我们的$(objects)值变成：“main.o foo.o bar.o utils.o another.o”（another.o被追加进去了）</p><br /><p>五、override 指示符:<br />如果有变量是通常make的命令行参数设置的，那么Makefile中对这个变量的赋值会被忽略。如果你想在Makefile中设置这类参数的值，那么，你可以使用“override”指示符。其语法是：<br />override &lt;variable&gt; = &lt;value&gt;<br />override &lt;variable&gt; := &lt;value&gt;<br />当然，你还可以追加：<br />override &lt;variable&gt; += &lt;more text&gt;<br />对于多行的变量定义，我们用define指示符，在define指示符前，也同样可以使用ovveride指示符，如：<br />override define foo<br />bar<br />endef</p><br /><p>六、多行变量<br />还有一种设置变量值的方法是使用define关键字。使用define关键字设置变量的值可以有换行，这有利于定义一系列的命令（前面我们讲过“命令包”的技术就是利用这个关键字）<br />define 指示符后面跟的是变量的名字，而重起一行定义变量的值，定义是以endef关键字结束。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字，或是其它变量。因为命令需要以[Tab]键开头，所以如果你用define定义的命令变量中没有以[Tab]键开头，那么make就不会把其认为是命令。<br />下面的这个示例展示了define的用法：<br />define two-lines<br />echo foo<br />echo $(bar)<br />endef</p><br /><p>七、环境变量<br />make 运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中，但是如果Makefile中已定义了这个变量，或是这个变量由make命令行带入，那么系统的环境变量的值将被覆盖。（如果make指定了“-e”参数，那么，系统环境变量将覆盖Makefile中定义的变量）</p><br /><p>八、目标变量<br />前面我们所讲的在Makefile中定义的变量都是“全局变量”，在整个文件，我们都可以访问这些变量。当然，“自动化变量”除外，如“$&lt;”等这种类量的自动化变量就属于“规则型变量”，这种变量的值依赖于规则的目标和依赖目标的定义。<br />用法：<br />&lt;target ...&gt; : &lt;variable-assignment&gt;<br />&lt;target ...&gt; : overide &lt;variable-assignment&gt;<br />如：<br />prog : CFLAGS = -g<br />prog : prog.o foo.o bar.o<br />$(CC) $(CFLAGS) prog.o foo.o bar.o</p><br /><p>九、模式变量<br />make的“模式”一般是至少含有一个“%”的<br />模式变量的语法和“目标变量”一样：<br />&lt;pattern ...&gt; : &lt;variable-assignment&gt;<br />&lt;pattern ...&gt; : override &lt;variable-assignment&gt;<br />使用条件判断<br />——————<br />一、示例<br />下面的例子，判断$(CC)变量是否“gcc”，如果是的话，则使用GNU函数编译目标。<br />libs_for_gcc = -lgnu<br />normal_libs =<br />foo: $(objects)<br />ifeq ($(CC),gcc)<br />$(CC) -o foo $(objects) $(libs_for_gcc)<br />else<br />$(CC) -o foo $(objects) $(normal_libs)<br />endif</p><p>二、语法<br />条件表达式的语法为：<br />&lt;conditional-directive&gt;<br />&lt;text-if-true&gt;<br />endif<br />以及：<br />&lt;conditional-directive&gt;<br />&lt;text-if-true&gt;<br />else<br />&lt;text-if-false&gt;<br />endif<br />其中&lt;conditional-directive&gt;表示条件关键字<br />第一个是“ifeq”&#160; &#160;比较参数“arg1”和“arg2”的值是否相同。<br />第二个是“ifneq”&#160; 其比较参数“arg1”和“arg2”的值是否相同，如果不同，则为真。和“ifeq”类似。<br />第三个是“ifdef”&#160; 如果变量&lt;variable-name&gt;的值非空，那到表达式为真。否则，表达式为假。<br />第四个是“ifndef” 和“ifdef”是相反的意思。</p><br /><p>使用函数<br />————<br />一、函数的调用语法<br />函数调用，很像变量的使用，也是以“$”来标识的，其语法如下：<br />$(&lt;function&gt; &lt;arguments&gt; )<br />或是<br />${&lt;function&gt; &lt;arguments&gt;}<br />这里，&lt;function&gt;就是函数名，make支持的函数不多。&lt;arguments&gt;是函数的参数，参数间以逗号“,”分隔，而函数名和参数之间以“空格”分隔。函数调用以“$”开头，以圆括号或花括号把函数名和参数括起。感觉很像一个变量，是不是？函数中的参数可以使用变量，为了风格的统一，函数和变量的括号最好一样，如使用“$(subst a,b,$(x))”这样的形式，而不是“$(subst a,b,${x})”的形式。因为统一会更清楚，也会减少一些不必要的麻烦。</p><p>二、字符串处理函数<br />$(subst &lt;from&gt;,&lt;to&gt;,&lt;text&gt; )<br />名称：字串替换函数——subst。<br />功能：把字串&lt;text&gt;中的&lt;from&gt;字符串替换成&lt;to&gt;。<br />返回：函数返回被替换过后的字符串。</p><br /><p>名称：模式字符串替换函数——patsubst。<br />功能：查找&lt;text&gt;中的单词（单词以“空格”、“Tab”或“回车”“换行”分隔）是否符合模式&lt;pattern&gt;，如果匹配的话，则以&lt;replacement&gt;替换。这里，&lt;pattern&gt;可以包括通配符“%”，表示任意长度的字串。如果&lt;replacement&gt;中也包含“%”，那么，&lt;replacement&gt;中的这个“%”将是&lt;pattern&gt;中的那个“%”所代表的字串。（可以用“\”来转义，以“\%”来表示真实含义的“%”字符）返回：函数返回被替换过后的字符串。</p><p>名称：去空格函数——strip。<br />功能：去掉&lt;string&gt;字串中开头和结尾的空字符。<br />返回：返回被去掉空格的字符串值。</p><p>名称：查找字符串函数——findstring。<br />功能：在字串&lt;in&gt;中查找&lt;find&gt;字串。<br />返回：如果找到，那么返回&lt;find&gt;，否则返回空字符串。</p><br /><p>名称：过滤函数——filter。<br />功能：以&lt;pattern&gt;模式过滤&lt;text&gt;字符串中的单词，保留符合模式&lt;pattern&gt;的单词。可<br />以有多个模式。<br />返回：返回符合模式&lt;pattern&gt;的字串。</p><p>名称：反过滤函数——filter-out。<br />功能：以&lt;pattern&gt;模式过滤&lt;text&gt;字符串中的单词，去除符合模式&lt;pattern&gt;的单词。可<br />以有多个模式。<br />返回：返回不符合模式&lt;pattern&gt;的字串。</p><p>名称：排序函数——sort。<br />功能：给字符串&lt;list&gt;中的单词排序（升序）。<br />返回：返回排序后的字符串。<br />示例：$(sort foo bar lose)返回“bar foo lose” 。</p><p>名称：取单词函数——word。<br />功能：取字符串&lt;text&gt;中第&lt;n&gt;个单词。（从一开始）<br />返回：返回字符串&lt;text&gt;中第&lt;n&gt;个单词。如果&lt;n&gt;比&lt;text&gt;中的单词数要大，那么返回空<br />字符串。</p><p>名称：取单词串函数——wordlist。<br />功能：从字符串&lt;text&gt;中取从&lt;s&gt;开始到&lt;e&gt;的单词串。&lt;s&gt;和&lt;e&gt;是一个数字。<br />返回：返回字符串&lt;text&gt;中从&lt;s&gt;到&lt;e&gt;的单词字串。如果&lt;s&gt;比&lt;text&gt;中的单词数要大，那<br />么返回空字符串。如果&lt;e&gt;大于&lt;text&gt;的单词数，那么返回从&lt;s&gt;开始，到&lt;text&gt;结束的单<br />词串。</p><p>名称：单词个数统计函数——words。<br />功能：统计&lt;text&gt;中字符串中的单词个数。<br />返回：返回&lt;text&gt;中的单词数。<br />示例：$(words, foo bar baz)返回值是“3”。<br />备注：如果我们要取&lt;text&gt;中最后的一个单词，我们可以这样：$(word $(words &lt;text&gt; <br />),&lt;text&gt; )。</p><p>名称：首单词函数——firstword。<br />功能：取字符串&lt;text&gt;中的第一个单词。<br />返回：返回字符串&lt;text&gt;的第一个单词。<br />示例：$(firstword foo bar)返回值是“foo”。<br />备注：这个函数可以用word函数来实现：$(word 1,&lt;text&gt; )。</p><p>三、文件名操作函数<br />$(dir &lt;names...&gt; )</p><p>名称：取目录函数——dir。<br />功能：从文件名序列&lt;names&gt;中取出目录部分。目录部分是指最后一个反斜杠（“/”）之<br />前的部分。如果没有反斜杠，那么返回“./”。<br />返回：返回文件名序列&lt;names&gt;的目录部分。<br />示例： $(dir src/foo.c hacks)返回值是“src/ ./”。</p><p>名称：取文件函数——notdir。<br />功能：从文件名序列&lt;names&gt;中取出非目录部分。非目录部分是指最后一个反斜杠（“/”<br />）之后的部分。<br />返回：返回文件名序列&lt;names&gt;的非目录部分。<br />示例： $(notdir src/foo.c hacks)返回值是“foo.c hacks”。</p><p>名称：取后缀函数——suffix。<br />功能：从文件名序列&lt;names&gt;中取出各个文件名的后缀。<br />返回：返回文件名序列&lt;names&gt;的后缀序列，如果文件没有后缀，则返回空字串。<br />示例：$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。</p><p>名称：取前缀函数——basename。<br />功能：从文件名序列&lt;names&gt;中取出各个文件名的前缀部分。<br />返回：返回文件名序列&lt;names&gt;的前缀序列，如果文件没有前缀，则返回空字串。<br />示例：$(basename src/foo.c src-1.0/bar.c hacks)返回值是“src/foo src-1.0/bar h<br />acks”。</p><p>名称：加后缀函数——addsuffix。<br />功能：把后缀&lt;suffix&gt;加到&lt;names&gt;中的每个单词后面。<br />返回：返回加过后缀的文件名序列。<br />示例：$(addsuffix .c,foo bar)返回值是“foo.c bar.c”。</p><br /><p>名称：加前缀函数——addprefix。<br />功能：把前缀&lt;prefix&gt;加到&lt;names&gt;中的每个单词后面。<br />返回：返回加过前缀的文件名序列。<br />示例：$(addprefix src/,foo bar)返回值是“src/foo src/bar”。</p><p>名称：连接函数——join。<br />功能：把&lt;list2&gt;中的单词对应地加到&lt;list1&gt;的单词后面。如果&lt;list1&gt;的单词个数要比&lt;<br />list2&gt;的多，那么，&lt;list1&gt;中的多出来的单词将保持原样。如果&lt;list2&gt;的单词个数要比<br />&lt;list1&gt;多，那么，&lt;list2&gt;多出来的单词将被复制到&lt;list2&gt;中。<br />返回：返回连接过后的字符串。<br />示例：$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”。</p><p>四、foreach 函数<br />foreach 函数和别的函数非常的不一样。因为这个函数是用来做循环用的，Makefile中的<br />foreach函数几乎是仿照于Unix标准Shell（/bin /sh）中的for语句，或是C-Shell（/bin<br />/csh）中的foreach语句而构建的。它的语法是：<br />$(foreach &lt;var&gt;,&lt;list&gt;,&lt;text&gt; )<br />这个函数的意思是，把参数&lt;list&gt;中的单词逐一取出放到参数&lt;var&gt;所指定的变量中，然后再执行&lt;text&gt;所包含的表达式。每一次&lt;text&gt;会返回一个字符串，循环过程中，&lt;text&gt;的所返回的每个字符串会以空格分隔，最后当整个循环结束时，&lt;text&gt;所返回的每个字符串所组成的整个字符串（以空格分隔）将会是foreach函数的返回值。</p><p>五、if 函数<br />if函数很像GNU的make所支持的条件语句——ifeq（参见前面所述的章节），if函数的语法是：<br />$(if &lt;condition&gt;,&lt;then-part&gt; )<br />或是<br />$(if &lt;condition&gt;,&lt;then-part&gt;,&lt;else-part&gt; )</p><p>六、call函数<br />call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式，这个表达式中，你可以定义许多参数，然后你可以用call函数来向这个表达式传递参数。其语法是：<br />$(call &lt;expression&gt;,&lt;parm1&gt;,&lt;parm2&gt;,&lt;parm3&gt;...)<br />当 make执行这个函数时，&lt;expression&gt;参数中的变量，如$(1)，$(2)，$(3)等，会被参数&lt;parm1&gt;，&lt;parm2&gt;，&lt;parm3&gt;依次取代。而&lt;expression&gt;的返回值就是 call函数的返回值。</p><p>七、origin函数<br />注意，&lt;variable&gt;是变量的名字，不应该是引用。所以你最好不要在&lt;variable&gt;中使用“$”字符。Origin函数会以其返回值来告诉你这个变量的“出生情况”，下面，是origin函<br />数的返回值:<br />“undefined”<br />如果&lt;variable&gt;从来没有定义过，origin函数返回这个值“undefined”。<br />“default”</p><p>八、shell函数<br />shell 函数也不像其它的函数。顾名思义，它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说，shell函数把执行操作系统命令后的输出作为函数<br />返回。于是，我们可以用操作系统命令以及字符串处理命令awk，sed等等命令来生成一个变量，如：<br />contents := $(shell cat foo)<br />files := $(shell echo *.c)<br />注意，这个函数会新生成一个Shell程序来执行命令，所以你要注意其运行性能，如果你的Makefile中有一些比较复杂的规则，并大量使用了这个函数，那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。</p><p>九、控制make的函数<br />make提供了一些函数来控制make的运行。通常，你需要检测一些运行Makefile时的运行时信息，并且根据这些信息来决定，你是让make继续执行，还是停止。<br />$(error &lt;text ...&gt; )<br />产生一个致命的错误，&lt;text ...&gt;是错误信息。注意，error函数不会在一被使用就会产生错误信息，所以如果你把其定义在某个变量中，并在后续的脚本中使用这个变量，那么也<br />是可以的。</p><p>make 的运行<br />一、make的退出码<br />make命令执行后有三个退出码：<br />&#160; &#160; 0 —— 表示成功执行。<br />&#160; &#160; 1 —— 如果make运行时出现任何错误，其返回1。<br />&#160; &#160; 2 —— 如果你使用了make的“-q”选项，并且make使得一些目标不需要更新，那么返回2。</p><p>二、指定Makefile<br />前面我们说过，GNU make找寻默认的Makefile的规则是在当前目录下依次找三个文件——“GNUmakefile”、“makefile”和“Makefile”。其按顺序找这三个文件，一旦找到，就<br />开始读取这个文件并执行。<br />当前，我们也可以给make命令指定一个特殊名字的Makefile。要达到这个功能，我们要使用make的“-f”或是“--file”参数（“-- makefile”参数也行）。</p><p>三、指定目标<br />一般来说，make的最终目标是makefile中的第一个目标，而其它目标一般是由这个目标连带出来的。这是make的默认行为。当然，一般来说，你的 makefile中的第一个目标是由许多个目标组成，你可以指示make，让其完成你所指定的目标。要达到这一目的很简单，需在make命令后直接跟目标的名字就可以完成（如前面提到的“make clean”形式）任何在makefile中的目标都可以被指定成终极目标，但是除了以“- ”打头，或是包含了“=”的目标，因为有这些字符的目标，会被解析成命令行参数或是变量。甚至没有被我们明确写出来的目标也可以成为make的终极目标，也就是说，只要make可以找到其隐含规则推导规则，那么这个隐含目标同样可以被指定成终极目标。<br />有一个make的环境变量叫“MAKECMDGOALS”，这个变量中会存放你所指定的终极目标的列表，如果在命令行上，你没有指定目标，那么，这个变量是空值。这个变量可以让你使用在一些比较特殊的情形下。<br />&#160; &#160;“all”&#160; &#160; &#160; &#160; &#160; &#160; &#160; 这个伪目标是所有目标的目标，其功能一般是编译所有的目标。<br />&#160; &#160;“clean”&#160; &#160; &#160; &#160;这个伪目标功能是删除所有被make创建的文件。<br />&#160; &#160;“install”&#160; &#160; &#160; &#160;这个伪目标功能是安装已编译好的程序，其实就是把目标执行文件拷贝到指定的目标中去。<br />&#160; &#160;“print”&#160; &#160; &#160; &#160; &#160;这个伪目标的功能是例出改变过的源文件。<br />&#160; &#160;“tar”&#160; &#160; &#160; &#160; &#160; &#160; &#160;这个伪目标功能是把源程序打包备份。也就是一个tar文件。<br />&#160; &#160;“dist”&#160; &#160; &#160; &#160; &#160; &#160;这个伪目标功能是创建一个压缩文件，一般是把tar文件压成Z文件。或是gz文件。<br />&#160; &#160;“TAGS”&#160; &#160; &#160; &#160; 这个伪目标功能是更新所有的目标，以备完整地重编译使用。<br />&#160; &#160;“check”和“test”&#160; &#160; 这两个伪目标一般用来测试makefile的流程。</p><p>四、检查规则<br />有时候，我们不想让我们的makefile中的规则执行起来，我们只想检查一下我们的命令，或是执行的序列。于是我们可以使用make命令的下述参数：<br />“-n”<br />“--just-print”<br />“--dry-run”<br />“--recon”<br />不执行参数，这些参数只是打印命令，不管目标是否更新，把规则和连带规则下的命令打印出来，但不执行，这些参数对于我们调试makefile很有用处。<br />“-t”<br />“--touch”<br />这个参数的意思就是把目标文件的时间更新，但不更改目标文件。也就是说，make假装编译目标，但不是真正的编译目标，只是把目标变成已编译过的状态。<br />“-q”<br />“--question”<br />这个参数的行为是找目标的意思，也就是说，如果目标存在，那么其什么也不会输出，当然也不会执行编译，如果目标不存在，其会打印出一条出错信息。<br />“-W &lt;file&gt;”<br />“--what-if=&lt;file&gt;”<br />“--assume-new=&lt;file&gt;”<br />“--new-file=&lt;file&gt;”<br />这个参数需要指定一个文件。一般是是源文件（或依赖文件），Make会根据规则推导来运行依赖于这个文件的命令，一般来说，可以和“-n”参数一同使用，来查看这个依赖文件<br />所发生的规则命令。<br />另外一个很有意思的用法是结合“-p”和“-v”来输出makefile被执行时的信息</p><p>五、make的参数<br />“-b”<br />“-m”<br />这两个参数的作用是忽略和其它版本make的兼容性。<br />“-B”<br />“--always-make”<br />认为所有的目标都需要更新（重编译）。<br />“-C &lt;dir&gt;”<br />“--directory=&lt;dir&gt;”<br />指定读取makefile的目录。如果有多个“-C”参数，make的解释是后面的路径以前面的作为相对路径，并以最后的目录作为被指定目录。如：“make –C ~hchen/test –C prog”<br />等价于“make –C ~hchen/test/prog”。<br />“—debug[=&lt;options&gt;]”<br />输出make的调试信息。它有几种不同的级别可供选择，如果没有参数，那就是输出最简单的调试信息。下面是&lt;options&gt;的取值：<br />&#160; &#160; a —— 也就是all，输出所有的调试信息。（会非常的多）<br />&#160; &#160; b —— 也就是basic，只输出简单的调试信息。即输出不需要重编译的目标。<br />&#160; &#160; v —— 也就是verbose，在b选项的级别之上。输出的信息包括哪个makefile被解析，不需要被重编译的依赖文件（或是依赖目标）等。<br />&#160; &#160; i —— 也就是implicit，输出所以的隐含规则。<br />&#160; &#160; j —— 也就是jobs，输出执行规则中命令的详细信息，如命令的PID、返回码等。<br />&#160; &#160; m —— 也就是makefile，输出make读取makefile，更新makefile，执行makefile的信息。<br />“-d”<br />相当于“--debug=a”。<br />“-e”<br />“--environment-overrides”<br />指明环境变量的值覆盖makefile中定义的变量的值。<br />“-f=&lt;file&gt;”<br />“--file=&lt;file&gt;”<br />“--makefile=&lt;file&gt;”<br />指定需要执行的makefile。<br />“-h”<br />“--help”<br />显示帮助信息。<br />“-i”<br />“--ignore-errors”<br />在执行时忽略所有的错误。<br />“-I &lt;dir&gt;”<br />“--include-dir=&lt;dir&gt;”<br />指定一个被包含makefile的搜索目标。可以使用多个“-I”参数来指定多个目录。<br />“-j [&lt;jobsnum&gt;]”<br />“--jobs[=&lt;jobsnum&gt;]”<br />指同时运行命令的个数。如果没有这个参数，make运行命令时能运行多少就运行多少。如果有一个以上的“-j”参数，那么仅最后一个“-j”才是有效的。（注意这个参数在MS-D<br />OS中是无用的）<br />“-k”<br />“--keep-going”<br />出错也不停止运行。如果生成一个目标失败了，那么依赖于其上的目标就不会被执行了。<br />“-l &lt;load&gt;”<br />“--load-average[=&lt;load]”<br />“—max-load[=&lt;load&gt;]”<br />指定make运行命令的负载。<br />“-n”<br />“--just-print”<br />“--dry-run”<br />“--recon”<br />仅输出执行过程中的命令序列，但并不执行。<br />“-o &lt;file&gt;”<br />“--old-file=&lt;file&gt;”<br />“--assume-old=&lt;file&gt;”<br />不重新生成的指定的&lt;file&gt;，即使这个目标的依赖文件新于它。<br />“-p”<br />“--print-data-base”<br />输出makefile中的所有数据，包括所有的规则和变量。这个参数会让一个简单的makefile都会输出一堆信息。如果你只是想输出信息而不想执行 makefile，你可以使用“make -q<br />p”命令。如果你想查看执行makefile前的预设变量和规则，你可以使用“make –p –f /dev/null”。这个参数输出的信息会包含着你的makefile文件的文件名和行号，所以，用<br />这个参数来调试你的makefile会是很有用的，特别是当你的环境变量很复杂的时候。<br />“-q”<br />“--question”<br />不运行命令，也不输出。仅仅是检查所指定的目标是否需要更新。如果是0则说明要更新，如果是2则说明有错误发生。<br />“-r”<br />“--no-builtin-rules”<br />禁止make使用任何隐含规则。<br />“-R”<br />“--no-builtin-variabes”<br />禁止make使用任何作用于变量上的隐含规则。<br />“-s”<br />“--silent”<br />“--quiet”<br />在命令运行时不输出命令的输出。<br />“-S”<br />“--no-keep-going”<br />“--stop”<br />取消“-k”选项的作用。因为有些时候，make的选项是从环境变量“MAKEFLAGS”中继承下来的。所以你可以在命令行中使用这个参数来让环境变量中的“-k”选项失效。<br />“-t”<br />“--touch”<br />相当于UNIX的touch命令，只是把目标的修改日期变成最新的，也就是阻止生成目标的命令运行。<br />“-v”<br />“--version”<br />输出make程序的版本、版权等关于make的信息。<br />“-w”<br />“--print-directory”<br />输出运行makefile之前和之后的信息。这个参数对于跟踪嵌套式调用make时很有用。<br />“--no-print-directory”<br />禁止“-w”选项。<br />“-W &lt;file&gt;”<br />“--what-if=&lt;file&gt;”<br />“--new-file=&lt;file&gt;”<br />“--assume-file=&lt;file&gt;”<br />假定目标&lt;file&gt;需要更新，如果和“-n”选项使用，那么这个参数会输出该目标更新时的运行动作。如果没有“-n”那么就像运行UNIX的“touch”命令一样，使得&lt;file&gt;的修改时<br />间为当前时间。<br />“--warn-undefined-variables”<br />只要make发现有未定义的变量，那么就输出警告信息。</p><br /><p>隐含规则<br />————<br />隐含规则”也就是一种惯例，make会按照这种“惯例”心照不喧地来运行，那怕我们的Makefile中没有书写这样的规则。</p><p>一、使用隐含规则<br />如果要使用隐含规则生成你需要的目标，你所需要做的就是不要写出这个目标的规则。那么，make会试图去自动推导产生这个目标的规则和命令，如果make可以自动推导生成这个目标的规则和命令，那么这个行为就是隐含规则的自动推导。当然，隐含规则是make事先约定好的一些东西。<br />make 会在自己的“隐含规则”库中寻找可以用的规则，如果找到，那么就会使用。如果找不到，那么就会报错。<br />还有，在make的“隐含规则库”中，每一条隐含规则都在库中有其顺序，越靠前的则是越被经常使用的，所以，这会导致我们有些时候即使我们显示地指定了目标依赖，make也不会管。</p><p>二、隐含规则一览<br />这里我们将讲述所有预先设置（也就是make内建）的隐含规则，如果我们不明确地写下规则，那么，make就会在这些规则中寻找所需要规则和命令。当然，我们也可以使用make的参数“-r”或“--no-builtin-rules”选项来取消所有的预设置的隐含规则。<br />当然，即使是我们指定了“-r”参数，某些隐含规则还是会生效，因为有许多的隐含规则都是使用了“后缀规则”来定义的，所以，只要隐含规则中有“后缀列表 ”（也就一系统<br />定义在目标.SUFFIXES的依赖目标），那么隐含规则就会生效。默认的后缀列表是：.out,.a, .ln, .o, .c, .cc, .C, .p, .f, .F, .r, .y, .l, .s, .S, .mod, .sym, .def, .<br />h, .info, .dvi, .tex, .texinfo, .texi, .txinfo, .w, .ch .web, .sh, .elc, .el。</p><p>三、隐含规则使用的变量<br />在隐含规则中的命令中，基本上都是使用了一些预先设置的变量。你可以在你的makefile中改变这些变量的值，或是在make的命令行中传入这些值，或是在你的环境变量中设置这些值，无论怎么样，只要设置了这些特定的变量，那么其就会对隐含规则起作用。当然，你也可以利用make的“-R”或“--no– builtin-variables”参数来取消你所定义的变量<br />对隐含规则的作用。</p><p>下面是所有隐含规则中会用到的变量：<br />1、关于命令的变量。<br />AR&#160; &#160;函数库打包程序。默认命令是“ar”。<br />AS<br />汇编语言编译程序。默认命令是“as”。<br />CC<br />C语言编译程序。默认命令是“cc”。<br />CXX<br />C++语言编译程序。默认命令是“g++”。<br />CO<br />从 RCS文件中扩展文件程序。默认命令是“co”。<br />CPP<br />C程序的预处理器（输出是标准输出设备）。默认命令是“$(CC) –E”。<br />FC<br />Fortran 和 Ratfor 的编译器和预处理程序。默认命令是“f77”。<br />GET<br />从SCCS文件中扩展文件的程序。默认命令是“get”。<br />LEX<br />Lex方法分析器程序（针对于C或Ratfor）。默认命令是“lex”。<br />PC<br />Pascal语言编译程序。默认命令是“pc”。<br />YACC<br />Yacc文法分析器（针对于C程序）。默认命令是“yacc”。<br />YACCR<br />Yacc文法分析器（针对于Ratfor程序）。默认命令是“yacc –r”。<br />MAKEINFO<br />转换Texinfo源文件（.texi）到Info文件程序。默认命令是“makeinfo”。<br />TEX<br />从TeX源文件创建TeX DVI文件的程序。默认命令是“tex”。<br />TEXI2DVI<br />从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是“texi2dvi”。<br />WEAVE<br />转换Web到TeX的程序。默认命令是“weave”。<br />CWEAVE<br />转换C Web 到 TeX的程序。默认命令是“cweave”。<br />TANGLE<br />转换Web到Pascal语言的程序。默认命令是“tangle”。<br />CTANGLE<br />转换C Web 到 C。默认命令是“ctangle”。<br />RM<br />删除文件命令。默认命令是“rm –f”。</p><br /><p>2、关于命令参数的变量<br />下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值，那么其默认值都是<br />空。<br />ARFLAGS<br />函数库打包程序AR命令的参数。默认值是“rv”。<br />ASFLAGS<br />汇编语言编译器参数。（当明显地调用“.s”或“.S”文件时）。<br />CFLAGS<br />C语言编译器参数。<br />CXXFLAGS<br />C++语言编译器参数。<br />COFLAGS<br />RCS命令参数。<br />CPPFLAGS<br />C预处理器参数。（ C 和 Fortran 编译器也会用到）。<br />FFLAGS<br />Fortran语言编译器参数。<br />GFLAGS<br />SCCS “get”程序参数。<br />LDFLAGS<br />链接器参数。（如：“ld”）<br />LFLAGS<br />Lex文法分析器参数。<br />PFLAGS<br />Pascal语言编译器参数。<br />RFLAGS<br />Ratfor 程序的Fortran 编译器参数。<br />YFLAGS<br />Yacc文法分析器参数。</p><p>四、隐含规则链<br />有些时候，一个目标可能被一系列的隐含规则所作用。例如，一个[.o]的文件生成，可能会是先被Yacc的[.y]文件先成[.c]，然后再被C的编译器生成。我们把这一系列的隐含规则<br />叫做“隐含规则链”</p><p>五、定义模式规则<br />你可以使用模式规则来定义一个隐含规则。一个模式规则就好像一个一般的规则，只是在规则中，目标的定义需要有&quot;%&quot;字符。&quot;%&quot;的意思是表示一个或多个任意字符。在依赖目标中同样可以使用&quot;%&quot;，只是依赖目标中的&quot;%&quot;的取值，取决于其目标。</p><p>1、模式规则介绍<br />模式规则中，至少在规则的目标定义中要包含&quot;%&quot;，否则，就是一般的规则。目标中的&quot;%&quot;定义表示对文件名的匹配，&quot;%&quot;表示长度任意的非空字符串。例如：&quot;%.c&quot;表示以&quot;.c&quot;结尾的文件名（文件名的长度至少为3），而&quot;s.%.c&quot;则表示以&quot;s.&quot;开头，&quot;.c&quot;结尾的文件名（文件名的长度至少为 5）</p><p>2、模式规则示例<br />%.o : %.c<br />$(CC) -c $(CFLAGS) $(CPPFLAGS) $&lt; -o $@<br />其中，&quot;$@&quot;表示所有的目标的挨个值，&quot;$&lt;&quot;表示了所有依赖目标的挨个值。这些奇怪的变<br />量我们叫&quot;自动化变量&quot;，后面会详细讲述</p><p>3、自动化变量<br />$@<br />表示规则中的目标文件集。在模式规则中，如果有多个目标，那么，&quot;$@&quot;就是匹配于目标中模式定义的集合。<br />$%<br />仅当目标是函数库文件中，表示规则中的目标成员名。例如，如果一个目标是&quot;foo.a(bar.o)&quot;，那么，&quot;$%&quot;就是&quot;bar.o&quot;，&quot;$@&quot;就是&quot;foo.a&quot;。如果目标不是函数库文件（Unix下是<br />[.a]，Windows下是[.lib]），那么，其值为空。</p><p>$&lt;<br />依赖目标中的第一个目标名字。如果依赖目标是以模式（即&quot;%&quot;）定义的，那么&quot;$&lt;&quot;将是符合模式的一系列的文件集。注意，其是一个一个取出来的。<br />$?<br />所有比目标新的依赖目标的集合。以空格分隔。<br />$^<br />所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的，那个这个变量会去除重复的依赖目标，只保留一份。<br />$+<br />这个变量很像&quot;$^&quot;，也是所有依赖目标的集合。只是它不去除重复的依赖目标。<br />$*<br />这个变量表示目标模式中&quot;%&quot;及其之前的部分。如果目标是&quot;dir/a.foo.b&quot;，并且目标的模式是&quot;a.%.b&quot;，那么，&quot;$*&quot;的值就是&quot;dir /a.foo&quot;。这个变量对于构造有关联的文件名是比<br />较有较。如果目标中没有模式的定义，那么&quot;$*&quot;也就不能被推导出，但是，如果目标文件的后缀是 make所识别的，那么&quot;$*&quot;就是除了后缀的那一部分。例如：如果目标是&quot;foo.c&quot;<br />，因为&quot;.c&quot;是make所能识别的后缀名，所以，&quot;$*&quot;的值就是&quot;foo&quot;。这个特性是GNU make的，很有可能不兼容于其它版本的make，所以，你应该尽量避免使用&quot;$*&quot;，除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的，那么&quot;$*&quot;就是空值。<br />当你希望只对更新过的依赖文件进行操作时，&quot;$?&quot;在显式规则中很有用，<br />$(@D)<br />表示&quot;$@&quot;的目录部分（不以斜杠作为结尾），如果&quot;$@&quot;值是&quot;dir/foo.o&quot;，那么&quot;$(@D)&quot;就是&quot;dir&quot;，而如果&quot;$@&quot;中没有包含斜杠的话，其值就是&quot;.&quot;（当前目录）。<br />$(@F)<br />表示&quot;$@&quot;的文件部分，如果&quot;$@&quot;值是&quot;dir/foo.o&quot;，那么&quot;$(@F)&quot;就是&quot;foo.o&quot;，&quot;$(@F)&quot;相当于函数&quot;$(notdir $@)&quot;。<br />&quot;$(*D)&quot;<br />&quot;$(*F)&quot;<br />和上面所述的同理，也是取文件的目录部分和文件部分。对于上面的那个例子，&quot;$(*D)&quot;返回&quot;dir&quot;，而&quot;$(*F)&quot;返回&quot;foo&quot;<br />&quot;$(%D)&quot;<br />&quot;$(%F)&quot;<br />分别表示了函数包文件成员的目录部分和文件部分。这对于形同&quot;archive(member)&quot;形式的目标中的&quot;member&quot;中包含了不同的目录很有用。<br />&quot;$(&lt;D)&quot;<br />&quot;$(&lt;F)&quot;<br />分别表示依赖文件的目录部分和文件部分。<br />&quot;$(^D)&quot;<br />&quot;$(^F)&quot;<br />分别表示所有依赖文件的目录部分和文件部分。（无相同的）<br />&quot;$(+D)&quot;<br />&quot;$(+F)&quot;<br />分别表示所有依赖文件的目录部分和文件部分。（可以有相同的）<br />&quot;$(?D)&quot;<br />&quot;$(?F)&quot;<br />分别表示被更新的依赖文件的目录部分和文件部分。<br />最后想提醒一下的是，对于&quot;$&lt;&quot;，为了避免产生不必要的麻烦，我们最好给$后面的那个特定字符都加上圆括号，比如，&quot;$(&lt; )&quot;就要比&quot;$&lt;&quot;要好一些。<br />还得要注意的是，这些变量只使用在规则的命令中，而且一般都是&quot;显式规则&quot;和&quot;静态模式规则&quot;（参见前面&quot;书写规则&quot;一章）。其在隐含规则中并没有意义。</p><p>4、模式的匹配<br />一般来说，一个目标的模式有一个有前缀或是后缀的&quot;%&quot;，或是没有前后缀，直接就是一个&quot;%&quot;。因为&quot;%&quot;代表一个或多个字符，所以在定义好了的模式中，我们把&quot;%&quot;所匹配的内容叫做&quot;茎&quot;，例如&quot;%.c&quot;所匹配的文件&quot;test.c&quot;中&quot;test&quot;就是&quot;茎&quot;。因为在目标和依赖目标中同时有&quot;%&quot;时，依赖目标的&quot;茎&quot;会传给目标，当做目标中的&quot;茎&quot;。<br />当一个模式匹配包含有斜杠（实际也不经常包含）的文件时，那么在进行模式匹配时，目录部分会首先被移开，然后进行匹配，成功后，再把目录加回去。在进行&quot;茎&quot;的传递时，我们需要知道这个步骤。例如有一个模式&quot;e%t&quot;，文件&quot;src/eat&quot; 匹配于该模式，于是&quot;src/a&quot;就是其&quot;茎&quot;，如果这个模式定义在依赖目标中，而被依赖于这个模式的目标中又有个模式&quot;c%r&quot;，那么，目标就是&quot;src/car&quot;。（&quot;茎&quot;被传递）</p><p>5、重载内建隐含规则<br />你可以重载内建的隐含规则（或是定义一个全新的），例如你可以重新构造和内建隐含规则不同的命令，如：<br />%.o : %.c<br />$(CC) -c $(CPPFLAGS) $(CFLAGS) -D$(date)<br />你可以取消内建的隐含规则，只要不在后面写命令就行。如：<br />%.o : %.s<br />同样，你也可以重新定义一个全新的隐含规则，其在隐含规则中的位置取决于你在哪里写下这个规则。朝前的位置就靠前。</p><p>六、老式风格的&quot;后缀规则&quot;<br />后缀规则是一个比较老式的定义隐含规则的方法。后缀规则会被模式规则逐步地取代。因为模式规则更强更清晰。为了和老版本的Makefile兼容，GNU make同样兼容于这些东西。后缀规则有两种方式：&quot;双后缀&quot;和&quot;单后缀&quot;。<br />双后缀规则定义了一对后缀：目标文件的后缀和依赖目标（源文件）的后缀。如&quot;.c.o&quot;相当于&quot;%o : %c&quot;。单后缀规则只定义一个后缀，也就是源文件的后缀。如&quot;.c&quot;相当于&quot;% : %.c&quot;。<br />后缀规则中所定义的后缀应该是make所认识的，如果一个后缀是make所认识的，那么这个规则就是单后缀规则，而如果两个连在一起的后缀都被make所认识，那就是双后缀规则。例如：&quot;.c&quot;和&quot;.o&quot;都是make所知道。因而，如果你定义了一个规则是&quot;.c.o&quot;那么其就是双后缀规则，意义就是&quot;.c&quot; 是源文件的后缀，&quot;.o&quot;是目标文件的后缀。</p><p>七、隐含规则搜索算法<br />比如我们有一个目标叫 T。下面是搜索目标T的规则的算法。请注意，在下面，我们没有提到后缀规则，原因是，所有的后缀规则在Makefile被载入内存时，会被转换成模式规则。如果目标是&quot;archive(member)&quot;的函数库文件模式，那么这个算法会被运行两次，第一次是找目标T，如果没有找到的话，那么进入第二次，第二次会把&quot;member&quot;当作T来搜索。<br />&#160; &#160; 1、把T的目录部分分离出来。叫D，而剩余部分叫N。（如：如果T是&quot;src/foo.o&quot;，那么，D就是&quot;src/&quot;，N就是&quot;foo.o&quot;）<br />&#160; &#160; 2、创建所有匹配于T或是N的模式规则列表。<br />&#160; &#160; 3、如果在模式规则列表中有匹配所有文件的模式，如&quot;%&quot;，那么从列表中移除其它的模式。<br />&#160; &#160; 4、移除列表中没有命令的规则。<br />&#160; &#160; 5、对于第一个在列表中的模式规则：<br />&#160; &#160; &#160; &#160; 1）推导其&quot;茎&quot;S，S应该是T或是N匹配于模式中&quot;%&quot;非空的部分。<br />&#160; &#160; &#160; &#160; 2）计算依赖文件。把依赖文件中的&quot;%&quot;都替换成&quot;茎&quot;S。如果目标模式中没有包含斜框字符，而把D加在第一个依赖文件的开头。<br />&#160; &#160; &#160; &#160; 3）测试是否所有的依赖文件都存在或是理当存在。（如果有一个文件被定义成另外一个规则的目标文件，或者是一个显式规则的依赖文件，那么这个文件就叫&quot;理当存在&quot;）<br />&#160; &#160; &#160; &#160; 4）如果所有的依赖文件存在或是理当存在，或是就没有依赖文件。那么这条规则将被采用，退出该算法。<br />&#160; &#160; 6、如果经过第5步，没有模式规则被找到，那么就做更进一步的搜索。对于存在于列表中的第一个模式规则：<br />&#160; &#160; &#160; &#160; 1）如果规则是终止规则，那就忽略它，继续下一条模式规则。<br />&#160; &#160; &#160; &#160; 2）计算依赖文件。（同第5步）<br />&#160; &#160; &#160; &#160; 3）测试所有的依赖文件是否存在或是理当存在。<br />&#160; &#160; &#160; &#160; 4）对于不存在的依赖文件，递归调用这个算法查找他是否可以被隐含规则找到。<br />&#160; &#160; &#160; &#160; 5）如果所有的依赖文件存在或是理当存在，或是就根本没有依赖文件。那么这条规则被采用，退出该算法。<br />&#160; &#160; 7、如果没有隐含规则可以使用，查看&quot;.DEFAULT&quot;规则，如果有，采用，把&quot;.DEFAULT&quot;的命令给T使用。<br />一旦规则被找到，就会执行其相当的命令，而此时，我们的自动化变量的值才会生成。</p><br /><p>使用make更新函数库文件<br />———————————<br />一、函数库文件的成员<br />一个函数库文件由多个文件组成。你可以以如下格式指定函数库文件及其组成：<br />archive(member)<br />这个不是一个命令，而一个目标和依赖的定义。一般来说，这种用法基本上就是为了&quot;ar&quot;命令来服务的。如：<br />foolib(hack.o) : hack.o<br />ar cr foolib hack.o<br />如果要指定多个member，那就以空格分开，如：<br />foolib(hack.o kludge.o)</p><p>二、函数库成员的隐含规则<br />当 make搜索一个目标的隐含规则时，一个特殊的特性是，如果这个目标是&quot;a(m)&quot;形式的，其会把目标变成&quot;(m)&quot;。于是，如果我们的成员是&quot;%.o&quot; 的模式定义，并且如果我们使用&quot;make foo.a(bar.o)&quot;的形式调用Makefile时，隐含规则会去找&quot;bar.o&quot;的规则，如果没有定义bar.o的规则，那么内建隐含规则生效，make会去找bar.c文件来生成bar.o，如果找得到的话，make执行的命令大致如下：<br />cc -c bar.c -o bar.o<br />ar r foo.a bar.o<br />rm -f bar.o<br />还有一个变量要注意的是&quot;$%&quot;，这是专属函数库文件的自动化变量，有关其说明请参见&quot;自动化变量&quot;一节。</p><p>三、函数库文件的后缀规则<br />你可以使用&quot;后缀规则&quot;和&quot;隐含规则&quot;来生成函数库打包文件，如：<br />.c.a:<br />$(CC) $(CFLAGS) $(CPPFLAGS) -c $&lt; -o $*.o<br />$(AR) r $@ $*.o<br />$(RM) $*.o<br />其等效于：<br />(%.o) : %.c<br />$(CC) $(CFLAGS) $(CPPFLAGS) -c $&lt; -o $*.o<br />$(AR) r $@ $*.o<br />$(RM) $*.o</p><p>四、注意事项<br />在进行函数库打包文件生成时，请小心使用make的并行机制（&quot;-j&quot;参数）。如果多个ar命令在同一时间运行在同一个函数库打包文件上，就很有可以损坏这个函数库文件。所以，在make未来的版本中，应该提供一种机制来避免并行操作发生在函数打包文件上。<br />但就目前而言，你还是应该不要尽量不要使用&quot;-j&quot;参数。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:04:51 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=304&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[ltr]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=303&amp;action=new</link>
			<description><![CDATA[<p>装载任务状态段寄存器TR<br />&#160; &#160; 在任务内发生特权级变换时堆栈也随着自动切换，外层堆栈指针保存在内层堆栈中，而内层堆栈指针存放在当前任务的TSS中。所以，在从外层向内层变换时，要访问TSS(从内层向外层转移时不需要访问TSS，而只需内层栈中保存的栈指针)。实例在进入保护模式下的临时代码段后，通过如下两条指令装载任务状态段寄存器TR，使其指向已预置好的任务的TSS：</p><p> mov ax,SelectorTSS</p><p> ltr ax</p><br /><p>&#160; &#160; LTR指令是专门用于装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择子。LTR指令从GDT中取出相应的TSS段描述符，把TSS段描述符的基地址和界限等信息装入TR的高速缓冲寄存器中。</p><br /><br /><p>UD2指令，产生一个中断，该中断使处理器把eflags，cs，ip等压入堆栈。ud2指令的bochs调试地址为：0x30430</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:03:51 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=303&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[bochs 调试 com 文件]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=302&amp;action=new</link>
			<description><![CDATA[<p>在网上看到2中解决此问题的方法：<br />1.使用dos下的debug32工具单步跟踪pmtest2.com的运行情况。但这个方法存在问题，当跟踪到mov cr0，eax语句时freedos会错误，无法继续跟踪下去。<br />2.使用bochs的magic_break，方法如下：<br />在Bochs的配置文件里加上一句magic_break: enabled=1，然后在程序里加上一句xchg bx,bx，运行到这条指令时会断到Bochs调试器里(在Bochsrc_sample.txt里)。<br />网上的留言的大侠使用该方法成功了，可惜我没成功，网上回了个贴，说了一下自己的操作流程，等待回复中。<br />今天终于把这个方法实验成功了，前面失败的原因是没有把新编译成的pmtest.com文件拷贝到pm.img文件中。下面把整个步骤完整的列一下：<br />1)修改bochsrc文件。<br />在文件末尾增加“magic_break: enabled=1”<br />2)pmtest.asm文件中增加语句xchg&#160; bx, bx，选了2个地方都没有成功<br />org&#160; 0100h<br />&#160; ;xchg&#160; bx, bx&#160; &#160; &#160; &#160; &#160;;我选的位置</p><p>&#160; jmp&#160; LABEL_BEGIN</p><br /><br /><p>3)用nasm编译新该的asm文件，并拷贝到pm.img中<br />sudo mount -o loop pm.img /mnt<br />sudo cp pmtest.com /mnt<br />sudo umount /mnt<br />4)启动bochs<br />bochs -f bochsrc<br />5)选择6<br />Please choose one: [6]<br />让bochs运行<br />&lt;bochs:1&gt; c&#160; 回车<br />6)在freedos中进入b盘，运行pmtest.com<br />7)在bochs的调试窗口中将会停在xchg bx,bx处。提示如下：<br /> (0) Magic breakpoint<br />&#160; &#160;Next at t=89456572<br />&#160; &#160;(0) [0x00032342] 3224:0102 (unk. ctxt): jmp .+643 (0x000325c8)&#160; &#160; ; e98302<br />&#160; &#160;&lt;bochs:2&gt;<br />这样我们又可以用前面的方法单步调试程序了。<br />3.使用bochs的vb命令和 Edit options实现<br />参考http://blog.csdn.net/titer1/archive/2010/04/28/5540362.aspx，上面图文并茂讲的很清楚。<br />vb命令需要seg:ofs格式来设置断点，那如何知道seg和ofs值呢？<br />在网上看到一个方法：在你想下断点的位置前增加语句jmp $，当pmtest在dos下运行起来后回到bochs调试窗口，按下ctrl+c，bochs将会停在jmp $的位置，并显示出seg:ofs。我们就可以用这个值和vb来下断点了。<br />费力的折腾了一周，才过了这关。希望作者再版时把这部分内容加上，好让大家能集中精力学习操作系统。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:03:12 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=302&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[bochs调试方法与指令详解]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=301&amp;action=new</link>
			<description><![CDATA[<p>bochs调试FAQ:<br />一 基本调试命令</p><p>1.&#160; &#160; &#160; &#160;Q:如何从引导扇区开始设置断点?</p><p>A: BIOS被载入内存中运行,其引导扇区的地址一般都是在0x7c00,因为在实模式下,逻辑地址和物理地址是一一对应的, 所以我们可以在启动bochs调试的命令行下输入</p><p>&#160; &#160; &#160; &#160; pb 0x7c00 或者 vb 0:0x7c00</p><p>就着输入</p><p>&#160; &#160; &#160; &#160; c</p><p>bochs就会在0x7c00处停下 </p><p>2.&#160; &#160; &#160; &#160;Q: 我想在单步执行(s)后看到寄存器的变化情况, 该怎么设置?</p><p>A: 输入</p><p>&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;trace-reg on</p><p>&#160; &#160;后, 再执行单步调试的时候都会显示寄存器的当前状态了.</p><p> </p><p>3.&#160; &#160; &#160; &#160;Q: 在单步调试的时候,遇到一个调用子程序的指令,输入s会把调试跳进子程序中,如何只执行子程序而不要陷进去调试?</p><p>A: 在调试子程序的指令处,不用s改为n或p</p><p>4.&#160; &#160; &#160; &#160;Q: 查看当前堆栈的命令?</p><p>A: print-stack</p><p> Table 1. 部分Bochs调试指令</p><p>行为<br />指令<br />举例<br />在某物理地址设置断点<br />b addr<br />b 0x30400显示当前所有断点信息<br />info break继续执行，直到遇上断点<br />c单步执行<br />s单步执行（遇到函数则跳过）<br />n查看寄存器信息<br />info cpu<br />r<br />fp<br />sreg<br />creg<br />info cpu<br />r<br />fp<br />sreg<br />creg<br />查看堆栈<br />print-stack查看内存物理地址内容<br />xp /nuf addr<br />xp /40bx 0x9013e<br />查看线性地址内容<br />x /nuf addr<br />x /40bx 0x13e<br />反汇编一段内存<br />u start end<br />u 0x30400 0x3040D<br />反汇编执行的每一条指令<br />trace-on每执行一条指令就打印CPU信息<br />trace-reg<br />trace-reg on<br />其中&quot;xp /40bx 0x9013e&quot;这样的格式可能显得有点复杂，读者可以用&quot;help x&quot;这一指令在Bochs中亲自看一下它代表的意义</p><p>二执行控制<br />c|cont 向下执行，相当于WinDBG的“g”。 <br />s|step|stepi [count] 单步执行，相当于WinDBG的“t”，count 默认为 1。 <br />p|n|next 单步执行，类似于WinDBG的“p”。 <br />q|quit|exit 退出调试，同时关闭虚拟机。 <br />Ctrl-C 结束执行状态，返回调试器提示符。 <br />Ctrl-D if at empty line on command line, exit<br />（至少在Windows版本中我没有发现Ctrl-D有什么功能）</p><p> <br />三 执行断点<br />vb|vbreak [seg:off] 在虚拟地址上下断点。 <br />lb|lbreak [addr] 在线性地址上下断点，相当于WinDBG的“bp”。 <br />pb|pbreak|b|break [addr] 在物理地址上下断点。（为了兼容GDB的语法，地址前可以加上一个“*”）。 <br />blist 显示断点状态，相当于WinDBG的“bl”。 <br />bpd|bpe [num] 禁用/启用断点，WinDBG的“be”和“bd”。num是断点号，可以用blist命令查询。 <br />d|del|delete [num] 删除断点，相当于WinDBG的“bc”。mum是断点号，可<br />以用blist命令查询。<br />四 读写断点<br />watch read [addr] 设置读断点。<br />watch write [addr] 设置写断点。<br />unwatch read [addr] 清除读断点。<br />unwatch write [addr] 清除写断点。<br />watch 显示当前所有读写断点。<br />unwatch 清除当前所有读写断点。<br />watch stop|continue 开关选项，设置遇到读写断点时中断下来还是显示出来但是继续运行。<br />五 内存操作 <br />x /nuf [addr] 显示线性地址的内容<br />xp /nuf [addr] 显示物理地址的内容<br />n 显示的单元数<br />u 每个显示单元的大小，u可以是下列之一：<br />b BYTE<br />h WORD<br />w DWORD<br />g DWORD64<br />注意: 这种命名法是按照GDB习惯的，而并不是按照inter的规范。<br />f 显示格式，f可以是下列之一：<br />x 按照十六进制显示<br />d 十进制显示<br />u 按照无符号十进制显示<br />o 按照八进制显示<br />t 按照二进制显示<br />c 按照字符显示<br />n、f、u是可选参数，如果不指定，则u默认是w，f默认是x。如果前面使用过x或<br />者xp命令，会按照上一次的x或者xp命令所使用的值。n默认为1。addr 也是一个<br />可选参数，如果不指定，addr是0，如过前面使用过x或者xp命令，指定了n=i，<br />则再次执行时n默认为i+1。 <br />setpmem [addr] [size] [val] 设置物理内存某地址的内容。 <br />需要注意的是，每次最多只能设置一个DWORD：<br />这样是可以的：<br />&lt;bochs:1&gt; setpmem 0x00000000 0x4 0x11223344<br />&lt;bochs:2&gt; x /4 0x00000000<br />[bochs]:<br />0x00000000 &lt;bogus+ 0&gt;: 0x11223344 0x00000000 0x00000000 0x00000000<br />这样也可以：<br />&lt;bochs:1&gt; setpmem 0x00000000 0x2 0x11223344<br />&lt;bochs:2&gt; x /4 0x00000000<br />[bochs]:<br />0x00000000 &lt;bogus+ 0&gt;: 0x00003344 0x00000000 0x00000000 0x00000000<br />或者：<br />&lt;bochs:1&gt; setpmem 0x00000000 0x1 0x20<br />&lt;bochs:2&gt; x /4 0x00000000<br />[bochs]:<br />0x00000000 &lt;bogus+ 0&gt;: 0x00000020 0x00000000 0x00000000 0x00000000<br />下面的做法都会导致出错：<br />&lt;bochs:1&gt; setpmem 0x00000000 0x3 0x112233<br />Error: setpmem: bad length value = 3<br />&lt;bochs:2&gt; setpmem 0x00000000 0x8 0x11223344<br />Error: setpmem: bad length value = 8 <br />crc [start] [end] 显示物理地址start到end之间数据的CRC。 </p><p> 六 寄存器操作<br />set $reg = val 设置寄存器的值。现在版本可以设置的寄存器包括：<br />eax ecx edx ebx esp ebp esi edi<br />暂时不能设置：<br />eflags cs ss ds es fs gs <br />r|reg|registers reg = val 同上。 <br />dump_cpu 显示完整的CPU信息。 <br />set_cpu 设置CPU状态，这里可以设置dump_cpu所能显示出来的所有CPU状态。 </p><p>七 反汇编命令 <br />u|disas|disassemble [/num] [start] [end]<br />反汇编物理地址start到end 之间的代码，如<br />果不指定参数则反汇编当前EIP指向的代码。<br />num是可选参数，指定处理的代码量。<br />set $disassemble_size = 0|16|32 $disassemble_size变量指定反汇编使用的段大小。 <br />set $auto_disassemble = 0|1 $auto_disassemble决定每次执行中断下来的时候（例如遇到断点、Ctrl-C等）是否反汇编当前指令。</p><p>八 其他命令<br />trace-on|trace-off Tracing开关打开后，每执行一条指令都会将反汇编的结果<br />显示出来。 <br />ptime 显示Bochs自本次运行以来执行的指令条数。 <br />sb [val] 再执行val条指令就中断。val是64-bit整数，以L结尾，形如“1000L” <br />sba [val] 执行到Bochs自本次运行以来的第val条指令就中断。val是64-bit整数，以L结尾，形如“1000L” <br />modebp 设置切换到v86模式时中断。 <br />record [&quot;filename&quot;] 将输入的调试指令记录到文件中。文件名必须包含引号。 <br />playback [&quot;filename&quot;] 回放record的记录文件。文件名必须包含引号。 <br />print-stack [num] 显示堆栈，num默认为16，表示打印的条数。 <br />?|calc 和WinDBG的“?”命令类似，计算表达式的值。 <br />load-symbols [global] filename [offset]<br />载入符号文件。如果设定了“global”关键字，则符号针对所有上下文都有效。offset会默认加到所有的symbol地址上。symbol文件的格式为：&quot;%x %s&quot;。</p><p>九 info命令<br />info program 显示程序执行的情况。<br />info registers|reg|r 显示寄存器的信息。<br />info pb|pbreak|b|break 相当于blist<br />info dirty 显示脏页的页地址。<br />info cpu 显示所有CPU寄存器的值。<br />info fpu 显示所有FPU寄存器的值。<br />info idt 显示IDT。<br />info gdt [num] 显示GDT。<br />info ldt 显示LDT。<br />info tss 显示TSS。<br />info pic 显示PIC。<br />info ivt [num] [num] 显示IVT。<br />info flags 显示状态寄存器。<br />info cr 显示CR系列寄存器。<br />info symbols 显示symbol信息。<br />info ne2k|ne2000 显示虚拟的ne2k网卡信息。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:02:48 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=301&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[颜色]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=300&amp;action=new</link>
			<description><![CDATA[<p>0-&gt;40*25 黑白&#160; <br />&#160; 1-&gt;40*25 彩色&#160; <br />&#160; 2-&gt;80*25 黑白&#160; <br />&#160; 3-&gt;80*25 彩色文本&#160; <br />&#160; 4-&gt;320*200 彩色&#160; <br />&#160; 5-&gt;320*200 黑白&#160; <br />&#160; 6-&gt;640*200 黑白图形模式&#160; <br />&#160; 7-&gt;80*25 单色字符单色显示器</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:02:27 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=300&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[为什么dos下的com文件都要org 0100h呢？为什么系统启动时要org 07c00h呢]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=299&amp;action=new</link>
			<description><![CDATA[<p>这是因为 .com&#160; &#160;载入内存后的起始偏址就是100h.&#160; &#160;前面的100h字节是该程序的PSP 部分.&#160; &#160;所以, 为了程序中对地址引用的正确,&#160; &#160;必需加上org&#160; &#160;100h语句。<br />-------------------------------------------------------------------------------------------------------------------------------------------------<br />FFFF0h与07C00h，这两个都是机器启动后默认访问的内存地址。曾经让我一度很晕，搞不清他们之间的关系。现在终于搞明白了，<br />首先要知道bios是用来初始化硬件的最底层的软件（然后才是操作系统），因此计算机启动后必须最先被执行。另外我们都知道CPU只能执行内存中的内容的，而一般内存中的数据是易失性的，断电之后内容就会消失。工程师的解决方法是：将存放bios的rom芯片与内存芯片统一编址（不明白的话去看看微机原理与接口的书就明白了）。这样就可以把存放bios的ROM芯片看作是数据永远不会消失不允许被更改的内存了。<br />开机启动后默认的CS=FFFFh IP=0000h。这个地址就是bios的地址。这段内存空间很小，所以不能够容下操作系统等大型程序。<br />相对bios而言操作系统的功能更强大，更新也更快但是也需要更多的空间，通常放在硬盘中。但是如果没有bios的话，那么将会连硬盘都不能使用，又如何启动存放在硬盘中的操作系统呢？正是由于此，机器启动后自动执行bios使其它完成硬件初始化（这样包括硬盘在内的cpu以外的其他硬件设备就可以工作了）。bios完成硬件初始化的任务后，就要把权力移交给操作系统。<br />工程师进行了强制性的规定：到内存中的07c00h 处寻找系统的引导程序，即CS=0000h IP=7c00h。也就是说任何系统，他的引导程序都必须安排在07c00h开始的地方，否则就不能被正确的引导。当引导程序完成后我们就进入了Linux Windows等系统了。<br />后面为了调试方便，还可以吧org 07c00h改为0100h,生成 com文件就能直接在dos下运行。cx表示的是字符串的个数，10进制的，可以自行修改，boot sector的结束地址也是固定的，为0xAA55,呵呵，好玩<br />书中的代码如果要在DOS下运行，需要把org 07C00h改为org 0100h，这样做的目的是因为在DOS下，装载程序会事先创建一个称为程序段前缀（PSP）256字节的数据区，用来和被装载的程序通信，具体的细节请查验相关资料。而0100h刚好是256个字节的处，这也是代码段开始执行处。 <br />&#160; &#160; &#160; &#160;说完程序的加载运行，让我们看看BIOS的10号中断处理子程序的显示功能。这里10号中断有很多子程序，书中用到了13子程序，此子程序就是显示字符串的，其中的参数如下：ES:BP=串地址；CX=串长度；DH=起始行；DL=起始列；BH=页号；BL=属性；AH=子程序编号，这里为13；这里BL是这样子定义的：闪烁 背景红 背景绿 背景蓝 高亮 前景红 前景绿 前景蓝（顺序为高字节到低字节）。这样BL=0Ch，二进制为：1100，由其定义可知为黑底高亮红字。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 26 Aug 2022 06:01:39 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=299&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[linux内存与扇区,磁盘的基础知识——扇区、柱面、磁道、族]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=25&amp;action=new</link>
			<description><![CDATA[<p>磁盘的物理结构</p><p>磁盘是由若干块坚硬金属材料制成的并涂以磁性介质的盘片压制而成。每个盘有两面，每面各有一个磁头，都可记录信息。要了解硬盘的物理结构，需要弄懂磁道、扇区、柱面、簇等几个概念.</p><p>磁道：磁盘上划分了很多个同心圆，这些同心圆就是磁道。但打开硬盘，用户不能看到这些，实际上磁道是被磁头磁化的同心圆。磁道之间是有间隔的，因为磁化单元太近会产生干扰.</p><p>扇区：每条磁道被分成若干等份的区域，每个区域就是一片扇区。扇区是硬盘数据存储的最小单位.</p><p>柱面：假如一块磁盘只有3个磁盘片，每块盘片的磁道数是相等的.从外圈开始，这些磁道被分成了0磁道、1磁道、2磁道……。具有相同磁道编号的同心圆组成面就称作柱面。为了便于理解，柱面可以看作没有底的铁桶。柱面数就是磁盘上的磁道数。柱面是硬盘分区的最小单位.</p><p>一个硬盘的容量=柱面数(或磁道数)×磁头数×扇区数×每个扇区的大小(通常是512字节)</p><p>簇：扇区是硬盘数据存储的最小单位，但操作系统无法对数目众多的扇区进行寻址，所以操作系统就将相邻的扇区组合在一起，形成一个簇，然后再对簇进行管理.每个簇可以包括2、4、8、16、32、64个扇区。族有时也称作磁盘块(block)。它是属于文件系统层面的概念，是文件系统层中数据存储的基本单位。</p><p>磁盘的分区</p><p>要掌握硬盘的分区，需要掌握MBR、扩展分区、逻辑分区的概念.</p><p>硬盘里分为两个区域，一个是放置该硬盘的信息区，称之为主引导扇区(master boot recorder,MBR)，一个是实际文件数据放置的地方。其中，MBR是整个磁盘最重要的区域。一旦MBR物理实体损坏时，则该磁盘就差不多报废了.一般来说，MBR有512个字节，且可以分为两个部分.</p><p>第一部分有446个字节，用于存放引导代码，即bootloader.</p><p>第二部分有64个字节，用于存放磁盘分区表(Disk Partion Table,DPT)。每个分区的信息需要用16个字节来记录，因此，一块磁盘最多可以有4个分区。</p><p>注：通常所说的&quot;磁盘分区&quot;就是指修改磁盘分区表。它定义了&quot;第n个磁盘块是从第x个柱面到第y个柱面&quot;.因此，当系统要读取第n个磁盘块时，就是去读硬盘上第x个柱面到第y个柱面的信息.</p><p>分区有两种，一种是主分区，一种是扩展分区。在一块磁盘中，扩展分区只能有一个，所以这4个分区可以是4个主分区或者3个主分区加1个扩展分区，如下所示:</p><p>P + P + P + P</p><p>P + P + P + E</p><p>其中P表示主分区(Primary)，E表示扩展分区(Extend)</p><p>重点说明的是，扩展分区不能直接使用，还需要将其划分为逻辑分区.这样就产生了一个问题，既然扩展分区不能直接使用，但为什么还要划分出一定的空间来给扩展分区呢?这是因为，如果用户想要将硬盘划分为5个分区的话，那该如何? 此时，就需要扩展分区来帮忙了.</p><p>由于MBR仅能保存4个分区的数据信息，如果超过4个，系统允许在额外的硬盘空间存放另一份磁盘分区信息，这就是扩展分区. 若将硬盘分成3P+E，则E实际上是告诉系统，磁盘分区表在另外的那份分区表，即扩展分区其实是指向正确的额外分区表.本身扩展分区不能直接使用，还需要额外将扩展分区分成逻辑分区才能使用，因此，用户通过扩展分区就可以使用5个以上的分区了.</p><p>说明：</p><p>不建议用户将硬盘分为4个主分区.这是因为，假如一个20GB的硬盘，若4个主分区占据了15GB的空间，则剩下的5GB空间完全不能使用，因为已经没有多余的分区表可以记录这些空间了.</p><p>考虑到磁盘的连续性，一般建议将扩展分区放在最后面的柱面内.</p><p>允许一个硬盘只有1个主分区，其它空间都分配给扩展分区。</p><p>Linux磁盘分区</p><p>Windows操作系统是先将物理地址分开，再在分区上建立目录，因此在Windows操作系统中，所有路径都是从盘符开始，如C://program file。而Linux正好相反。在Linux操作系统中，先有目录，再将物理地址映射到目录中。在Linux操作系统中，所有路径都是从根目录开始.Linux默认可分为3个分区，分别是boot分区、swap分区和根分区.</p><p>无论是Windows操作系统，还是Linux操作系统，每个分区均可以有不同的文件系统，如FAT32、NTFS、Yaffs2等.</p><p>下面简单介绍一下Linux的3个分区，boot分区、swap分区和根分区。</p><p>boot分区</p><p>该分区对应于/boot目录，约100MB.该分区存放Linux的Grub(bootloader)和内核源码.用户可通过访问/boot目录来访问该分区.换句话说，用户对/boot目录的操作就是操作该分区.</p><p>swap分区</p><p>该分区没有对应的目录，故用户无法访问。Linux下的swap分区即为虚拟内存.虚拟内存用于当系统内存空间不足时，先将临时数据存放在swap分区，等待一段时间后，然后再将数据调入到内存中执行.所以说，虚拟内存只是暂时存放数据，在该空间内并没有执行.</p><p>关于虚拟内存</p><p>虚拟内存是指将硬盘上某个区域模拟为内存.因此虚拟内存的实际物理地址仍然在硬盘上.虚拟内存，或者说swap分区只能由系统访问，其大小为物理内存的2倍.</p><p>根分区</p><p>在Linux操作系统中，除/boot目录外的其它所有目录都对应于该分区.因此，用户可通过访问除/boot目录外的其它所有目录来访问该分区.</p><p>说明：</p><p>在Linux操作系统中，用户可根据需要把分区挂载到某个目录下。比如/home目录下有三个目录，假设为test1,test2,test3，可把不同的分区分别挂载到这三个目录下 。</p><p>根据上面的讨论，一块磁盘上最多有13个分区，即硬盘分为1个主分区和3个扩展分区，而每个扩展分区又可以分为4个逻辑分区。</p><p>硬盘上至少要有1个主分区.</p><p>逻辑分区不能再进行分区.</p><p>Linux分区目录和&quot;盘符&quot;的关系</p><p>假如硬盘安装在IDE1的主盘，并用户想分区成6个可以使用的硬盘分区，则可以采用3个主分区加3个逻辑分区方式，或者是1个主分区加5个逻辑分区。</p><p>Linux允许使用fdisk -l命令和df -h命令来查询其硬盘分区.其中，df无法显示出swap分区的大小.</p><p>若硬盘的MBR已坏，则该磁盘就不能再作为引导盘，只能作为数据盘.因为MBR位于硬盘的起始处，用户不能通过软件进行修复，也不能跳过起始处.而硬盘中间的某个磁道坏了，用户可以软件修复，也可以跳过该磁道.</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Thu, 04 Aug 2022 14:46:22 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=25&amp;action=new</guid>
		</item>
	</channel>
</rss>
