系统移植基础知识
一、基础概念
1.1 系统移植
系统
操作系统(本质一个程序一堆代码;管理对应的资源和任务)
移植
将源代码(Linux操作系统原码)从一种环境(开发环境)放在另一种环境(产品环境)下也能运行叫做移植
内核裁剪
make menuconfig;选择对应的功能
linux文件系统的瘦身
给动态库瘦身:删静态库加上调试信息
1.2 启动流程
pc机
1、BIOS:基本输入输出系统
初始化时钟;初始化内存;基本硬件的初始化
判断启动方式(USB;硬盘;光驱)
2、引导程序:
固化在硬盘(存储数据)最前面的部分
识别对应的操作系统
加载对应的操作系统
3、操作系统(没有图形化界面的程序)
4、文件系统(NTFS FAT(16/32)EXT(2、3、4);window系统常见的文件系统格式 日志文件系统)
5、应用程序(桌面运用程序)
PC机启动时,首先启动BIOS(负责系统时钟和内存),然后选择启动方式(USB、硬盘、光驱)。启动盘中最开始的程序是引导程序(在内存中运行),引导程序会识别系统数量:如果只有一个系统则直接加载;如果有多个操作系统,则让用户选择启动哪个系统。系统启动后,挂载文件系统并启动应用程序。操作系统本身是一个非图形化界面的程序,桌面是对应的应用程序。
嵌入式设备启动流程

1、厂家固化在IROM里面的代码
基本硬件初始化(需要的时钟和设备)
判断对应的启动方式(emmc;sd卡)
把bootloader(给操作系统准备环境)第一阶段的代码加载到IRAM(运行内存)
问:为什么不在IROM初始化内存
答:因为IROM是厂家过来的;并不知道你开发板采用的什么内存;无法初始化
2、在IRAM里运行对应的bootloader第一阶段的代码(SPL:第二段程序加载)
初始化整个系统的时钟;
初始化内存
把完整的bootloader搬移到内存中
问:为什么bootloader要分两个阶段运行;不直接在IRam里面运行还要搬运到内存里
答:因为IRAM在芯片内部;存储空间有限;如果内存太大很浪费资源
3、在内存里运行bootloader第二个阶段的代码
初始化了一些硬件设备(根据开发板去定义(网卡;串口;USB)
加载对应的操作系统(内存)
4、运行操作系统(没有图形化界面)
5、挂载文件系统(EXT(2、3、4))
6、启动应用程序

PC机和嵌入式设备的联系
bootloader=BIOS+引导程序
在所有的操作系统中苹果的任务调度、进程线程调度是最好的;用几年都不卡
linux集成的通信协议栈是最丰富的
1.3 交叉编译
不同的架构涉及到不同的编译器
arm-----arm-linux-gcc; x86------gcc
大部分都是arm架构
课上用到交叉编译器是x86的;但是这个程序编译出来的可执行文件是arm架构
APP想要运行要有相对应的架构(X86;arm;RISC-V);32/64;库
ELF 是 Executable and Linkable Format(可执行与可链接格式),是 Linux/Unix 系统中目标文件、共享库和可执行文件的标准文件格式。
readelf hello_x86 -a:显示对应的头部信息(Arm去头);把这些信息给删除
./arm-linux-gcc -v:运行当前目录下的 ARM 交叉编译器,并显示它的详细版本和配置信息。
-v : verbose 的缩写,意思是显示详细信息。
which gcc:可以直接查找gcc在哪
1.4 修改环境变量
1.echo $PATH:查找所有环境变量;
环境变量在执行命令运行代码的时候,在对应的路径下去查找对应的命令;类似于把指令搞成了全局变量
export PATH=$PATH:加路径:增加新的环境变量;注:只适用于当前终端。 关闭终端后设置就失效了。
2.想要所有终端都有效用.bashrc(当前linux用户登录后自动执行)
sudo vi .bashrc进入到脚本中
加入
TOOL_CHAIN=/home/hqyj/fs4412/tool_chain/gcc-4.6.4/bin export PATH=$PATH:$TOOL_CHAIN
就能对当前用户下所有的终端都有效
3.对全部用户都有效
sudo vi /etc/bash.bashrc
然后在文件中输入方法二的添加环境变量
source /etc/bash.bashrc
执行脚本
sudo su切换用户
1.5 开发板和PC机进行文件传输
在linux下搭建一个服务器
sudo apt-get install tftp-hpa(客户端程序)(用来进行对应测试)
sudo apt-get install tftpd-hpa(服务端程序)
桥接模式是将虚拟机的虚拟网卡直接连接到物理主机的物理网卡上,使其在局域网中拥有独立的 IP 地址,像一台真实电脑一样与局域网中的其他设备通信。
创建一个文件夹;这个文件夹用来保存tftp服务器的文件
先创建好文件夹(当成服务器的数据文件夹)后;给他对应的权限;然后再文件夹中加入文件
mkdir tf tpboot sudo chmod 777 -R tf tpboot
配置服务器的配置文件
sudo vi /etc/default/tftpd-hpa

重启tftp对应的服务
sudo service tftpd-hpa restart
测试(本地回环)linux运行了服务器;
tftp 本地回环地址:和tftp进行数据交换
get 文件名;获取文件(通过tftp服务器下载文件)
quit:退出交互
pc和板子链接方式
方法一:串口传输(速度太慢)
┌─────────────────┐ ┌─────────────────┐ │ PC 机 │ │ 开发板 │ │ │ │ │ │ MobaXterm │ │ UART 串口 │ │ (COM3 端口) │◄── 网线 ──►│ 调试口 │ │ │ │ │ └─────────────────┘ └─────────────────┘
方法二:网络通信(tftp:端口号69)
┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐ │ PC 机 │ │ 交换机 │ │ 开发板 │ │ │ │ (或路由器) │ │ │ │ TFTP/NFS 服务器│────────►│ 192.168.1.1│◄────────│ 网口 │ │ 192.168.1.100 │ │ │ │ 192.168.1.10 │ │ │ └─────────────┘ │ │ └─────────────────┘ └─────────────────┘
交换机会通过DHCP自动分配IP;但是不会给板子指定IP;所以板子需要自己配置IP;可能会IP重复
nfs服务器配置
NFS 服务器(/etc/exports)的共享配置,作用是:把主机上的 /home/linux/fs4412/rootfs 目录共享给网络中的所有设备,并设定访问权限。
# 1. 安装 NFS 服务器
sudo apt-get install nfs-kernel-server
# 2. 解压文件系统
tar -xvf rootf.tar.gz
# 3. 修改 NFS 配置文件(/etc/exports)
# 添加以下内容:
/home/linux/fs4412/rootfs *(rw,sync,no_root_squash)
# 4. 重启 NFS 服务
sudo service nfs-kernel-server restart
# 5. 测试挂载
sudo mount 127.0.0.1:/home/linux/fs4412/rootfs <空目录路径>
# 6. 卸载
sudo umount --lazy ./Public
通过网络挂载对应的文件系统;就不需要再下载对应的文件系统镜像;只需要下载内核的镜像和设备树
setenv bootcmd tftp 0x41000000 uImage\;tftp 0x42000000 exynos4412-fs4412.dtb\;bootm 0x41000000 - 0x42000000
没找到文件系统会卡死;linux系统启动后会找相对应的文件系统
把文件从 TFTP 服务器加载到内存(RAM)(临时的),然后从内存启动
setenv bootcmd tftp 0x41000000 uImage\;tftp 0x42000000 exynos4412-fs4412.dtb\;tftp 0x43000000 ramdisk.img\;bootm 0x41000000 0x43000000 0x42000000
在板子上创建的文件没有办法进行保存;因为运行在内存中;断电就消失
在开发阶段如果采用这个方式,代码没办法保存
让虚拟机的linux和开发板上的Linux共享一个文件系统(nfs);在虚拟机上写代码;在开发板上进行测试
最后把整个文件系统打包成镜像文件固化到emmc中
nfs下可以被远程访问的文件目录
/home/hqyj/fs4412/rootfs *(rw,sync,no_root_squash)
nfs和传统方式区别
用 NFS 可以直接挂载并编辑文件,修改即时生效,无需打包镜像和烧录;否则,每次修改文件系统都需要重新制作镜像、下载到开发板,整个流程繁琐且低效。
具体对比:
-
NFS 方式:把宿主机上的目录通过网络挂载为开发板的根文件系统,可以直接在宿主机上修改文件,开发板重启即生效,适合开发阶段。
-
传统方式:每次修改内容后,都要重新制作文件系统镜像(如 ramdisk.img),再通过 TFTP 下载或烧录到开发板,反复操作耗时且不利于调试。
| 对比维度 | 命令一(传统烧录) | 命令二(网络开发) |
|---|---|---|
| 完整命令 | setenv bootcmd movi read kernel 0x41000000;movi read dtb 0x42000000;movi rootfs 0x43000000 30000;bootm 0x41000000 - 0x42000000 |
setenv bootcmd tftp 41000000 Image\;tftp 42000000 exynos4412-fs4412.dtb\;tftp 43000000 ramdisk.img\;bootm 41000000 43000000 42000000 |
| 数据来源 | 板载存储(eMMC/NAND) | 网络服务器(TFTP) |
| 依赖条件 | 无需网络,文件已烧录到板载存储 | 必须联网,依赖 TFTP 服务器 |
| 修改文件系统 | 需重新制作镜像并烧录,过程繁琐 | 替换服务器文件即可,无需烧录 |
| 修改内核/设备树 | 需重新烧录到存储分区 | 替换 TFTP 服务器文件即可 |
| 启动速度 | 快(从本地存储读取) | 相对慢(需网络传输下载) |
| 适用阶段 | 量产/产品交付阶段 | 开发调试阶段 |
| 核心特征 | 从存储分区读取并启动 | 从网络下载内存并启动 |
1.6 基础命令解析
U-boot只是bootloader一种
print:打印对应的参数信息
setenv:设置对应参数信息;
serverip:服务器IP即linux的IP
ipaddr:板子IP
gatewayip:网关IP:192.168.2.1
ethaddr:mac地址
saveenv:保存设置
先ping网关再ping服务器
| 角色 | U-Boot 环境变量 | bootargs 中设置 |
|---|---|---|
| 开发板 IP | ipaddr=192.168.2.98 |
ip=192.168.2.98 |
| NFS 服务器 IP | serverip=192.168.2.101 |
nfsroot=192.168.2.1 |
| 网关 | gatewayip=192.168.2.1 |
未设置 |
U-Boot 支持自动将 ipaddr 的值填入 bootargs。
下载(手动)
1. 下载 Linux 镜像文件
tftp 0x41000000 uImage
将 Linux 内核镜像文件下载到内存地址 0x41000000(运行内存ARM)
2. 下载文件系统镜像文件
tftp 0x43000000 ramdisk.img
将文件系统镜像文件下载到内存地址 0x43000000(运行内存ARM)
3. 下载设备树文件
tftp 0x42000000 exynos4412-fs4412.dtb
将设备树文件下载到内存地址 0x42000000
4. 启动内核
bootm 0x41000000 0x43000000 0x42000000
从指定内存地址启动:内核地址 0x41000000、文件系统地址 0x43000000、设备树地址 0x42000000
bootcmd:uboot启动完成读秒后,自动执行从参数(命令)
1.7 bootargs参数讲解
bootargs告诉文件系统在哪里找;本质uboot(bootloader)给linux系统传递的-参数
setenv bootargs root=/dev/nfs nfsroot=192.168.2.101:/home/hqyj/fs4412/rootfs,proto=tcp,nfsvers=3,no_lock rw console=ttySAC2,115200 init=/linuxrc ip=192.168.2.98
| 参数 | 含义 |
|---|---|
root=/dev/nfs |
指定根文件系统(rootfs)不是从本地块设备(如 emmc/SD 卡)挂载,而是使用 NFS 网络文件系统。内核会因此启用 NFS 根文件系统支持。 |
nfsroot=192.168.2.101:/home/hdyj/fs4412/rootfs |
NFS 服务器的 IP 和导出路径: - 服务器 IP: 192.168.2.101- 共享目录: /home/hdyj/fs4412/rootfs(主机上存放根文件系统的位置) |
proto=tcp |
NFS 使用 TCP 协议(而非 UDP),TCP 更可靠,适合大文件传输和长距离网络。 |
nfsvers=3 |
使用 NFS 版本 3(常见于嵌入式开发,兼容性好)。 |
noLock |
禁用 NFS 文件锁(lockd)。避免在无锁服务时出现警告或延迟,常用于简单开发环境。 |
rw |
根文件系统以 读写(read-write) 方式挂载,允许在开发过程中修改文件。 |
console=ttySAC2,115200 |
内核控制台输出到 串口 2(ttySAC2),波特率 115200。这是开发板与主机间调试信息的输出通道。 |
init=/linuxrc |
内核启动后执行的第一个用户态程序是 /linuxrc(通常是 BusyBox 提供的初始化脚本或软链接到 /sbin/init)。 |
ip=192.168.2.98 |
给开发板自身指定静态 IP 地址(192.168.2.98),用于 NFS 挂载及网络通信。若不设,也可用 DHCP(ip=dhcp)。 |
1.8 Linux内核支持的传递格式
<1>通过struct parm struct结构体来传递(Linux 2.6之前使用的方式,已经过时,不便于扩展)
<2>通过tag列表来传递(Linux 2.6开始使用的方式)

不是链式的;地址是连续的
参数格式固定且不够灵活,增加或修改参数需要修改内核和 U‑Boot 双方的代码,扩展性差,难以支持硬件差异越来越大的新设备
<3>通过设备树来传递(Linux 3.0开始使用的方式)
u_boot修改了设备树的节点(bootargs参数节点和内存信息节点),把需要传递的信息放在设备树中。在r2寄存器中记录了设备树在内存中的地址。
注意:
-
在Linux 3.0之后的内核,这些方式都支持。
-
可以参看u-boot-2013-learn/arch/arm/lib/bootm.c文件了解参数传递过程
1.9 EMMC 的分区结构
EMMC 存储空间被划分成多个分区,每个分区有特定用途,类似电脑硬盘的 C 盘、D 盘:
| 分区名 | 作用 | 存储内容 | 存储介质 | 启动中的作用 |
|---|---|---|---|---|
| uboot | U-Boot 分区 | 存放 U-Boot 本身(u-boot.bin) |
EMMC / NAND Flash | 第一级启动:CPU 上电后首先从这里加载 U-Boot 到内存运行 |
| kernel | 内核分区 | 存放 Linux 内核镜像(uImage / zImage) |
EMMC / NAND Flash | 第二级启动:U-Boot 从这里读取内核到内存,然后启动内核 |
| dtb | 设备树分区 | 存放设备树文件(exynos4412-fs4412.dtb) |
EMMC / NAND Flash | 内核启动时读取:内核根据设备树信息识别硬件设备 |
| ramdisk | 临时根文件系统分区 | 存放 ramdisk.img(压缩的内存文件系统镜像) |
EMMC / 内存(TFTP加载) | 早期启动:内核先加载 ramdisk 作为临时根文件系统,然后切换到真正的 rootfs |
| system / rootfs | 永久根文件系统分区 | 存放完整的根文件系统(bin/, etc/, lib/, usr/, home/ 等) |
EMMC / NAND Flash / NFS | 最终根文件系统:内核最终挂载这里作为系统的根目录 / |
| 命令 | 数据来源 | 大小参数 |
|---|---|---|
movi write kernel 41000000 |
完整文件(uImage) | 不需要(U-Boot 自动识别文件大小) |
movi write rootfs 41000000 300000 |
原始数据(ramdisk.img 或 rootfs 镜像) | 需要(手动指定写入大小,单位是字节) |
二、内核
2.1 内核目录讲解
芯片厂家给到默认linux内核配置:arch/arm/configs
arch/arm/boot/compressed:linux对应的压/解;linux内核编译程序所占的内存大;但开发板的内存有限;所以需要把程序进行压缩;取出来的时候还要自解压
arch/arm/boot/dts:设备信息,源文件(给工程师看)dts相当于.c文件;dtsi相当于.h文件
设备树:.dtb:可执行文件(给操作系统用)
arch/arm/kernel/head.s:linux最先开始启动的代码
drivers/net/ethernet/davicom:物理网卡;dirvers:放各种驱动文件
net:网络协议栈对应的代码
2.2 makefile
1. 导入默认配置
make ARCH=arm exynos_defconfig
2. 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-
源码 (.c/.h/.S)
↓ 编译
目标文件 (.o)
↓ 链接
vmlinux(ELF 格式,完整内核)
↓ 处理
vmlinux.bin(纯二进制)
↓ 压缩
Image / zImage / uImage
编译后在同目录下生成vmlinux ;是编译生成的原始内核文件(未压缩、带调试信息),用于调试和分析,不能直接启动,最终会被加工成 uImage 或 zImage 才能烧录到开发板运行。
是ELF格式;包含头部信息;readelf -a vmlinux可以读头部信息
在arch/arm/boot 生成Image:去掉elf头和调试信息后的linux内核镜像;
和zImage (2.7M)(自解压代码 + gzip压缩后的去掉elf头之后的Linux内核镜像)
uImage是uboot专属镜像;就在zImage基础上多了64kb;多的信息给uboot用的;uboot去加载对应的操作系统
arch/arm/boot/compressed里的vmlinux:头部信息的linux镜像压缩文件+自解压代码
| 位置 | 文件 | 本质 |
|---|---|---|
根目录 /vmlinux |
原始内核 | 未压缩、ELF格式、带调试符号,是内核的主体 |
arch/arm/boot/compressed/vmlinux |
自解压程序 | 包含 压缩后的内核数据 + 自解压代码,不是内核本身 |
2.3 内核配置

另外两个支持都是固定的;其中网卡驱动是根据原理图上面的网卡芯片去找的,如果在menuconfig里面没有找到芯片的驱动;找对应的厂家要驱动适配。
2.4 添加新的驱动文件
1、把文件复制到drivers/char/文件;下
2、配置Kconfig和Makefile文件
3、在上级目录的Kconfig里添加source路径;在Makefile里加
4、在菜单里面使能
obj-$(CONFIG_HELLO) += hello/
注:
CONFIG_HELLO 是全局的
一旦在任何 Kconfig 中定义了 CONFIG_HELLO 并且被 source 包含进来,它就存在于整个内核配置系统中,所有 Makefile 都能使用它。
所以要删掉source "drivers/char/test/Kconfig"
2.5 编辑设备树
设备树将硬件信息从内核代码中解耦出来,以独立文件描述设备配置,内核只需读取设备树即可动态识别硬件,无需重新编译内核,大大提升了移植性和灵活性。
.dts == 源文件 .dtsi == 头文件 .dtb == 生成的文件(可执行文件)
在/arch/arm/boot/dts找到抄的母版;cp成自己的;
在Makefile里面添加设备树

在linux顶层的makefile里编辑设备树:make ARCH=arm CROSS_COMPILE=arm-linux- dtbs
gedit (以文本的形式打开)设备树文件添加网卡设备信息(添加设备节点)

三、Linux 内核配置与编译关系
-
menuconfig:Linux内核提供的菜单选项界面,表示内核支持哪些功能,好比去餐馆看到的菜单
-
.config:Linux内核当前选择的功能配置,记录了你具体点了哪些菜,好比点菜的凭证
-
Kconfig:为Linux内核提供菜单选项的定义,好比当前餐馆所有厨师会做的菜
-
Makefile:管理Linux内核的编译过程,根据.config里面的宏配置决定编译哪些代码,好比厨师根据点菜单做菜
Kconfig 定义了所有可选功能,menuconfig 读取 Kconfig 显示图形化菜单让你选择,你选好后保存到 .config 文件中,最后 Makefile 读取 .config 根据配置编译对应的代码。
3.1 Kconfig 配置选项语法
config 选项名
属性1
属性2
...
[1] 选项名
config HELLO
展现形式:CONFIG_HELLO,在 .config 文件中定义
[2] 属性
(1) 选择类型
|
类型 |
取值 |
含义 |
menuconfig 显示 |
|---|---|---|---|
|
tristate(三态) |
|
编译进内核 |
|
|
|
编译成模块( |
||
|
|
不编译 |
||
|
bool(两态) |
|
编译进内核 |
|
|
|
不编译 |
||
|
string |
字符串 |
如 |
|
|
int |
整数 |
如 |
|
|
hex |
十六进制数 |
如 |
(2) 提示字符串
prompt "提示字符串"
在配置菜单中显示的名称
(3) range
指定值的范围(用于 int 或 hex 类型)
(4) help
帮助信息,在 menuconfig 中按 ? 显示
help
"test help ..."
(5) default
当用户没有手动配置时,默认的选择值
config HELLO
tristate
prompt "hello support"
default y
(6) depends on — 依赖关系
depends on 配置选项名
depends on 配置选项名1(m) || 配置选项名2(y)
depends on 配置选项名1(y) && 配置选项名3(m)
依赖逻辑:
-
y= 2,m= 1,n= 0 -
&&→ 取最小值 -
||→ 取最大值
注意:
-
如果依赖结果为
0:选项不可见 -
如果依赖结果为
2:选项为三态(tristate) -
如果依赖结果为
1:选项为两态(bool)
(7) select — 自动选择
当前配置选项被选中的时候,同时自动选中 select 指定的配置选项
config DM9000
tristate "DM9000 support"
select CRC32
select MII
选 DM9000 时,自动选中 CRC32 和 MII。
(8) menuconfig — 菜单
menuconfig 选项名
表示这是一个菜单项,可以包含子选项。
menu "Test menu support"
config HELLO1
tristate
prompt "support HELLO1"
config HELLO2
tristate
prompt "support HELLO2"
endmenu
menuconfig TEST_MENUCONFIG
tristate
prompt "support menconfig"
if TEST_MENUCONFIG
config HELLO0
tristate
prompt "support HELLO0"
config HELLO0y
tristate
prompt "support HELLO0y"
endif
-
menu:纯组织作用,把选项归组显示(文件夹一样) -
menuconfig+if:父选项可以对菜单里的权限控制(y/m/n),父项什么态,子选项有相对应的权限
(9) choice — 二选一
代表多个选项中只能选一个。
choice
prompt "support choice"
optional
default CONFIG_TEST_C1
config CHOICE1
tristate "support choice1"
config CHOICE2
tristate "support choice2"
endchoice
(10) comment — 提示信息
在菜单中显示纯文本提示信息。
comment "以下为网络配置选项"
(11) source — 包含另一个 Kconfig
source "路径/另一个Kconfig"
将这个路径下的 Kconfig 文件内容包含进来,实现 Kconfig 文件的拆分和组织。
注意
(1) 和 (2) 必须有,其它可选择
config HELLO
tristate
prompt "Hello support"
help
"Compile hello.c"
config HELLO
tristate "Hello support"
---help---
"Compile hello.c" //可以不加引号
config HELLO1
bool
prompt "Hello1 support"
help
"Compile hello.c "
config HELLO2
string
prompt "Hello2 string"
help
"Compile hello.c "
config HELLO3
int
prompt "Hello3 int"
help
"Compile hello.c "
config HELLO4
int "Hello4 int" //03的简写
help
"Compile hello.c "
config HELLO5
int
prompt "Hello5 int rang[0-5]"
range 0 5
help
"Compile hello.c "
config HELLO6
hex
prompt "Hello6 hex"
help
"Compile hello.c "
config HELLO7
hex
prompt "Hello7 hex"
range 0 5
help
"Compile hello.c "
四、文件系统
制作的工具:
-
BusyBox — 文件系统(BusyBox 是一个集成了三百多个最常用Linux命令和工具的软件)
-
Buildroot — 文件系统、Linux内核、bootloader
-
Yocto — 文件系统、Linux内核、bootloader
BusyBox 编译安装流程
# 1. 配置(选择静态编译、交叉编译前缀等) make menuconfig Busybox Settings → Build Options(编译相关的一些选项)
-
[*] Build BusyBox as a static binary (no shared libs)
静态编译方式,这样就不需要采用动态库,直接拷贝过去即可运行 -
[ ] Force NOMMU build -
[ ] Build with Large File Support (for accessing files > 2 GB) -
(arm-linux-) Cross Compiler prefix
选择对应的编译器前缀 -
() Additional CFLAGS
| 编译方式 | 命令 | 特点 |
|---|---|---|
| 静态编译 | gcc hello.c -static -o hello |
库代码直接嵌入可执行文件,文件大,但独立运行,不依赖外部 .so |
| 动态编译 | gcc hello.c -o hello(不加 -static) |
可执行文件小,但运行时需要系统中有对应的动态库(.so) |
# 2. 编译(生成 busybox 可执行文件) make # 3. 安装(打包/部署) make install
把你之前的文件系统改名;创建一个rootfs;进入rootfs进行拷贝——install所有文件
linux操作系统跑起来后需要有一些文件夹进行资源的挂载
mkdir dev etc mnt proc var tmp sys root
板子跑起来还需要这些目录
寻找动态库
ldd查_x86架构涉及到的动态库;
找arm架构的动态库就readelf -a hello_arm在头部信息里面找
五、Busybox init启动过程

init === busybox === 会去读取 inittab(初始化表),并加载和运行里面的内容
inittab在etc文件夹里面

inittab 文件格式:
id编号:权限等级:动作:进程
即以什么编号、什么等级、什么动作去加载进程。
id编号和权限等级可以省略。
| 动作 | 说明 |
|---|---|
| sysinit | 表示进程在系统启动后最先执行,只执行一次,init 进程等待它结束才继续执行其他动作 |
| wait | 表示进程在执行 sysinit 后执行,只执行一次,init 进程等待它结束才继续执行其他动作 |
| once | 表示进程在执行 wait 后执行,只执行一次,init 进程不等待它结束 |
| respawn | 表示进程在启动 once 后执行,init 进程一旦发现该进程死掉,就会重新启动它 |
| askfirst | 表示进程在启动完 respawn 后执行,与 respawn 类似,不过 init 进程会先输出提示 "Please press Enter to activate this console",等用户输入回车后才启动子进程 |
| restart | 如果 BusyBox 中配置了 CONFIG_FEATURE_USE_INITTAB,并且 init 进程收到 SIGHUP 信号时,先重新读取/解析 /etc/inittab 文件,再执行该进程 |
| shutdown | 表示进程在系统收到重启、关闭系统命令时运行 |
| ctrlaltdel | 表示进程在按下 Ctrl+Alt+Del 组合键时运行 |
rcS
1. 内核启动
↓
2. 执行 /sbin/init(BusyBox 提供的 init)
↓
3. 读取 /etc/inittab
↓
4. 执行 ::sysinit:/etc/init.d/rcS
↓
5. rcS 脚本执行 /bin/mount -a
↓
6. mount -a 读取 /etc/fstab
↓
7. 挂载 fstab 中定义的所有文件系统
(proc、sysfs、tmpfs、dev 等)

proc — Linux内核的资源(进程资源) → /proc 这个目录
tmpfs — 临时存储的空间 → /tmp 这个目录
sysfs — Linux子系统(GPIO、PWM、I2C) → /sys 这个目录
tmpfs — Linux的设备挂载 → /dev 目录
挂载与创建
挂载设备是把存储设备或虚拟文件系统(如 /proc)挂到 Linux 目录树上,让系统能通过路径访问其内容,对应配置文件是 /etc/fstab,由 mount -a 读取执行。
创建设备是在 /dev 下生成设备节点文件,让应用程序通过该文件与硬件或内核驱动通信,由 /etc/init.d/rcS 中的 mdev -s 扫描 /sys 动态生成。
两者分工明确:挂载解决“怎么访问文件系统”,创建设备解决“怎么访问硬件设备”。挂载依赖 /etc/fstab,创建设备依赖 mdev。
sh
sh 就是给你提供一个能输入命令、与系统交互的命令行环境(Shell)。
init
通过看门狗机制监控 init 进程,若 init 因异常卡死而无法喂狗,系统将自动复位,避免设备永久锁死。
init(PID=1)
↓
读取 /etc/inittab
↓
执行 ::sysinit:/etc/init.d/rcS(初始化脚本)
↓
执行 ::askfirst:-/bin/sh(启动 Shell)
↓
/bin/sh 启动后自动读取 /etc/profile
↓
进入命令行交互界面(提示符出现)
系统启动时自动执行程序方法
(1) 在 bootargs 参数中指定自己的程序
-
init=/linuxrc默认写法 -
init=我们自己程序的路径
例如,需要执行的程序是 hello,路径为 /test/hello:
init=/test/hello
缺点:没有启动 init 进程
(2) 默认启动的是 init 进程,init 进程启动过程中读取 /etc/inittab 文件,并将该文件指定的程序启动起来,所以可以在此文件中添加自己的程序。
例如,需要执行的程序是 hello,路径为 /test/hello,并且该程序不能被杀死:
::respawn:/test/hello
(3) init 进程启动时,会执行 /etc/init.d/rcS 脚本,可以在此脚本中添加自己的程序。
例如:
/test/hello
或(后台运行):
/test/hello &
(4) init 进程启动时,会执行 /bin/sh,这是一个 shell 程序,shell 程序执行时会执行 /etc/profile 脚本文件,所以可以在该脚本中添加自己的程序。
例如:
/test/hello
或(后台运行):
/test/hello &
注意
很多时候会选择在 (2) 或 (4) 中添加:
| 方式 | 特点 |
|---|---|
| (2) /etc/inittab | 可以让自己的进程死掉后自动重新启动(respawn) |
| (4) /etc/profile | 可以让自己的程序在执行时,执行环境比较好(已加载环境变量) |
五、ramdisk文件系统(镜像文件)
1.拷贝镜像文件
dd 命令说明
dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。
|
参数 |
说明 |
|---|---|
|
|
输入文件名,缺省为标准输入,即指定源文件(input file) |
|
|
输出文件名,缺省为标准输出,即指定目的文件(output file) |
|
|
同时设置读入/输出的块大小为 bytes 个字节 |
|
|
一次转换 bytes 个字节,即指定转换缓冲区大小 |
示例命令
cd ~
dd if=/dev/zero of=ramdisk bs=1k count=8192
(ramdisk 为 8M)
/dev/zero 是 Linux 中的一个特殊设备文件(不是普通文件),它的特点是:
-
读取它时,永远返回 0(空字节)。
-
大小是无限的——你可以一直读,它一直输出 0。
ramdisk 为 8M
8192 × 1k = 8192KB = 8MB
它是根据对应的文件系统大小来决定的,一定要比对应的文件系统的大小大。这里文件系统大小约为 5.2M,所以选择了 8M 来确保有足够的空间容纳文件系统内容。
2.格式化镜像为 ext2
目的:添加文件系统的信息,操作系统起来之后才知道是什么文件系统。
mkfs.ext2 -F ramdisk
3.拷贝对应的文件系统文件到镜像中
之前的步骤中我们创建了一个文件系统,里面是空的,所以现在需要拷贝对应文件系统的文件到镜像中。
mkdir FileSystem ← 创建挂载点目录
sudo mount ramdisk FileSystem ← 将镜像挂载到目录
cd FileSystem/
sudo cp ../fs4412/rootfs/* ./ -a ← 拷贝根文件系统内容到镜像中
文件系统是“内容”(数据的组织方式),镜像文件是“载体”(存储这个内容的文件)。
用生活比喻来理解
| 概念 | 比喻 |
|---|---|
| 文件系统(如 ext2) | 图书的分类规则(书怎么编号、怎么摆放) |
| 镜像文件(ramdisk) | 一个空的纸箱(容器) |
| 格式化(mkfs.ext2) | 在纸箱里画好格子、贴好标签(写入文件系统规则) |
| 拷贝内容(cp) | 把书按规则放进纸箱里 |
| 最终产物 | 一个装好书、并按规则整理好的纸箱 = 可用的镜像文件 |
4.卸载
卸载是为了“封箱”——把镜像文件从系统挂载点断开,确保所有数据完整地写入到镜像文件中,这样才能得到一个可用的、一致的根文件系统镜像。 sudo umount FileSystem/
5.压缩 ramdisk 为 ramdisk.gz
目的:通过压缩可以把对应的 ramdisk 文件系统变得更小。
gzip --best -c ramdisk > ramdisk.gz
6.让U-Boot能识别
mkimage -n "ramdisk" -A arm -O linux -T ramdisk -C gzip -d ramdisk.gz ramdisk.img
这是一个 mkimage 命令(用于 U-Boot 制作镜像),用于将压缩后的 ramdisk 文件封装成 U-Boot 可识别的格式。
| 参数 | 含义 |
|---|---|
-n "ramdisk" |
设置镜像名称(Image Name)为 ramdisk |
-A arm |
指定架构(Architecture)为 ARM |
-O linux |
指定操作系统(OS)为 Linux |
-T ramdisk |
指定镜像类型(Type)为 ramdisk |
-C gzip |
指定压缩方式(Compression)为 gzip |
-d ramdisk.gz |
指定数据文件(Data file)为 ramdisk.gz |
ramdisk.img |
指定输出文件名为 ramdisk.img |
7.把镜像复制到目录中
cp ramdisk.img fs4412/tftpboot/
六、Linux 系统启动的三种部署策略
1.全部下载到内存执行(纯内存运行)
核心原理
U-Boot 通过 TFTP 协议,把所有需要的组件(内核、设备树、根文件系统)从网络服务器下载到开发板的内存(RAM)中,然后直接跳转到内存地址启动系统。整个过程不涉及板载存储,系统完全在内存中运行。
典型 U-Boot 命令
setenv bootcmd 'tftp 41000000 Image; tftp 42000000 exynos4412-fs4412.dtb; tftp 43000000 ramdisk.img; bootm 41000000 43000000 42000000'
优点
-
调试效率最高:修改内核、驱动或文件系统后,只需替换 TFTP 服务器上的文件,重启开发板即可生效。
-
零损耗:不会磨损板载 Flash。
缺点
-
依赖网络:每次启动都必须连接 TFTP 服务器。
-
断电即失:文件系统在内存中,修改不会保存(除非手动挂载存储)。
适用场景:驱动开发、内核移植、频繁修改文件系统的早期调试阶段。
2. 下载内核/设备树 + NFS 挂载文件系统(网络挂载)
核心原理
U-Boot 只通过 TFTP 下载内核和设备树到内存,但根文件系统不下载,而是通过 NFS(网络文件系统) 协议,将宿主机上的一个目录直接挂载为开发板的根目录 /。这意味着开发板的文件系统实际上存储在宿主机上。
典型 U-Boot 命令
setenv bootcmd 'tftp 41000000 Image; tftp 42000000 exynos4412-fs4412.dtb; bootm 41000000 - 42000000'
(在 bootargs 环境变量中指定 NFS 根目录)
setenv bootargs 'root=/dev/nfs nfsroot=192.168.1.100:/home/xxx/rootfs ip=192.168.1.10:192.168.1.100::255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200'
优点
-
文件修改即存:在宿主机上修改文件,保存后开发板立即可用,无需重启。
-
存储空间大:不受开发板 Flash 容量限制,可以挂载数十 GB 的根文件系统。
-
节省开发板资源:不占用板载存储空间。
缺点
-
强依赖网络:网络不稳定时系统会卡死。
-
启动速度最慢:需要从网络加载大量小文件。
适用场景:应用程序开发、根文件系统内容频繁调整的软件调试阶段。
3.全部固化到 eMMC 并从 eMMC 读取(本地存储启动)
核心原理
这是量产阶段的最终形态。所有组件(内核、设备树、根文件系统)都提前烧录到开发板的 eMMC/NAND Flash 中。U-Boot 启动时直接从存储介质的分区读取到内存,然后启动。完全不依赖网络。
典型 U-Boot 命令(eMMC 版)
setenv bootcmd 'movi read kernel 0x41000000; movi read dtb 0x42000000; movi read rootfs 0x43000000 30000; bootm 0x41000000 - 0x42000000'
(注:movi 是三星系列芯片读取存储的命令,其他平台可能用 mmc read 或 nand read)
优点
-
开机即用:上电无需外设,独立启动。
-
启动速度最快:从本地 Flash 读取速度远快于网络传输。
-
生产标准:符合产品出厂要求。
缺点
-
修改麻烦:任何文件变更都需要重新制作分区镜像并烧录(
dd或烧录工具)。 -
磨损风险:Flash 有写入寿命限制,不适合频繁刷写。
适用场景:产品定型后的量产阶段、用户现场部署。
终极对比(三张表总结)
|
对比维度 |
方式一:全 TFTP |
方式二:TFTP + NFS |
方式三:eMMC 固化 |
|---|---|---|---|
|
内核获取方式 |
TFTP 下载到内存 |
TFTP 下载到内存 |
eMMC 读取到内存 |
|
设备树获取方式 |
TFTP 下载到内存 |
TFTP 下载到内存 |
eMMC 读取到内存 |
|
根文件系统方式 |
TFTP 下载 ramdisk 到内存 |
NFS 网络挂载宿主机目录 |
eMMC 分区读取 |
|
修改是否方便 |
需替换镜像文件、重启 |
直接改,即改即用 |
需重新烧录 |
|
启动速度 |
中等(下载镜像) |
慢(小文件多) |
最快 |
|
网络依赖 |
强(启动全程依赖) |
极强(运行时也依赖) |
无 |
|
开发阶段 |
前期(内核/驱动调试) |
中期(应用/文件系统调试) |
后期(量产交付) |
一句话区分它们
-
方式一:最像“一次性”运行,所有文件从网络搬进内存跑,调试内核最快。
-
方式二:最像“本地电脑”,文件系统在服务器上,改完就能用,调试应用最爽。
-
方式三:最像“家电产品”,上电即用,不依赖任何外部环境,用户拿到的就是它。
七、U‑Boot
7.1 目录讲解
board目录
board/samsung/origen存放的是三星 Origen 开发板(Exynos 4210)的板级初始化代码和硬件配置文件
-
lowlevel_init.s:开发板上电后最先执行的汇编代码,负责关闭看门狗、设置栈指针、关闭中断等最基础的CPU环境。(时钟初始化) -
mem_setup.S:内存初始化汇编代码,负责配置DRAM控制器,让开发板能够正常使用外部内存(DDR)。(内存初始化)
common目录
存放的是 U-Boot / Linux 内核中与具体硬件无关的通用核心代码
common/ = 存放命令的“菜谱”(源代码)
drivers目录
驱动
net目录
网络协议栈;初始化网卡
arch目录
arch/arm/cpu/armv7/start.S 这个是uboot首先执行的代码
arch/arm/cpu/u-boot.lds 这个是连接脚本文件(告诉编译在连接生成可执行文件的时候,一些段存放位置)
Linux一个可执行文件
objcopy
| |
|elf 头 | ----------> 给Linux使用
|-------|
|.data |
|-------|
|.bss |
|-------|
|.ro |
|-------|
|.txt |
| 段 | 存储内容 | 是否占用可执行文件空间 | 是否占用内存空间(运行时) |
|---|---|---|---|
| .text | 机器码(你的代码) | 是(文件大小增加) | 是 |
| .rodata | 字符串常量、只读数据 | 是(文件大小增加) | 是 |
| .data | 已初始化的全局/静态变量 | 是(有初始值,得存) | 是(程序加载时复制到内存) |
| .bss | 未初始化的全局/静态变量 | 不占用(只记个长度) | 是(加载时由系统分配并清零) |
include目录
存放的是uboot相关代码需要头文件
include/configs/origen.h
origen开发板对应的头文件,它决定了origen所需要的一些代码的宏开关
它也是开发板配置头文件,开发板所需要的代码宏开关和相关的宏定义的
参数都应该在此文件中定义
boards.cfg目录
u-boot 支持的板子的配置信息,u-boot编译系统就是根据此配置文件,来识别当前板子的信息
U-Boot 编译生成文件说明
| 文件 | 说明 |
|---|---|
| u-boot.bin | raw binary 镜像(bootloader 第二阶段代码) |
| u-boot | ELF 二进制格式的镜像 |
| u-boot.sec | Motorola S-Record 格式的镜像 |
| u-boot.map | 记录了生成 u-boot 代码所需文件的路径 |
| System.map | 记录了 u-boot 中标签名和函数名对应的地址 |
7.2 两个最先开始的程序
| 对比维度 | linux/arch/arm/kernel/head.S |
u-boot/arch/arm/cpu/armv7/start.S |
|---|---|---|
| 所属软件 | Linux 内核 | U-Boot 引导程序 |
| 运行时机 | U-Boot 加载内核后,跳转到这里 | 开发板上电后,最先执行这里 |
| 主要任务 | 使能 MMU、建立内核页表、跳转到 start_kernel() |
关闭看门狗、设置中断向量、初始化 DRAM、搬移自身到内存、跳转到 board_init_f() |
| 最终目标 | 启动 Linux 操作系统 | 加载并启动 Linux 内核 |
| 运行环境 | 已由 U-Boot 初始化好 DRAM | 刚上电,CPU 处于原始状态(Flash/ROM 中执行) |
| 汇编/ C 混合 | 先汇编(head.S)后 C(start_kernel) | 先汇编(start.S)后 C(board_init_f/r) |
开发板上电
↓
U-Boot 最先执行
├── start.S(U-Boot 入口)
│ └── 初始化 CPU、DRAM、时钟,搬移 U-Boot 自身
↓
U-Boot 加载 Linux 内核和设备树到内存
↓
U-Boot 执行 bootm,跳转到内核入口
↓
Linux 内核执行
├── head.S(Linux 入口)
│ └── 使能 MMU、建立页表、准备 C 环境
↓
start_kernel()(Linux 内核 C 入口)
↓
挂载根文件系统,启动 init 进程
start.S是 U-Boot 的第一行代码(硬件初始化),head.S是 Linux 内核的第一行代码(CPU 运行环境准备)。一个是"给芯片穿衣服",一个是"让系统站起来"。
7.3 配置和编译 U-Boot 引导程序
指定架构
make origen_config
将 U-Boot 源码配置为针对三星 Origen 开发板(Exynos 4210)的编译环境。
会在u-boot-2013.01/include/config.mk里生成配置的板子信息;config.mk会在总的Makefile里加载
指定编译器
起始地址在System.map查看
| 阶段 | 文件 | 说明 |
|---|---|---|
| 第一阶段 | u-boot-spl.bin |
SPL(Secondary Program Loader),负责初始化最基础的硬件(如时钟、DRAM),将第二阶段的 u-boot.bin 加载到内存中 |
| 第二阶段 | u-boot.bin |
完整的 U-Boot,提供命令行、环境变量、网络、Flash 烧录等丰富功能,最终加载 Linux 内核 |
开发板上电
↓
ROM 固化代码(BL0)执行
↓
加载 SPL(u-boot-spl.bin)到 SRAM ← 第一阶段
↓
SPL 初始化 DRAM、时钟
↓
SPL 从存储介质(eMMC/SD/NAND)加载 u-boot.bin 到 DRAM ← 第二阶段
↓
跳转到 u-boot.bin 执行
↓
U-Boot 提供命令行,加载 Linux 内核
7.4 启动流程
ARM核的设置
-
设置CPU为SVC模式
-
设置异常向量表
-
关闭Cache和MMU(内存管理单元:A. 内存访问权限设置 B. 物理地址和虚地址的映射)
SOC芯片的设置
-
处理唤醒的条件
-
判断是否在内存中运行
-
如果不在内存中运行,则初始化系统时钟和内存控制器(第一阶段)
如果在内存中运行,则不需要初始化系统时钟和内存控制器(第二阶段) -
初始化 UART
如果通过 go 命令启动 u-boot 的时候,没有看到任何输出效果?
可能原因:
[1] u-boot 在内存中运行的时候,重新初始化了系统时钟和内存?
[2] 没有初始化串口?
[3] 指定内存基地址不对?
[4] ?(点灯大法:通过在一些位置加亮灯代码,确定导致 u-boot 死掉的代码)
[5] TrustZone 代码有问题?
在 u-boot-2013.01 for fs4412 上面,是由于 TrustZone 的代码有问题,我们只需要将board/samsung/origen/lowlevel_init.s
文件中的 bl tzpc_init 代码注释掉就可以了。
八、硬件底层代码调试方法
-
遇到硬件底层代码有问题,首先通信(打印信息)
-
如果通信有问题,只有点灯
找相关功能在哪;grep "board_eth_init" * -nR
在当前目录及其子目录下递归搜索 "board_eth_init",并在结果中显示行号(-n)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)