在完成rootfs的构建之后,通过配置内核支持Initramfs,并设置好rootfs的路径即可编译出带有文件系统的vmliunx

initramfs简介

是什么

initramfs 即 initram file system,翻译成中文意思就是 初始 ram 文件系统,基于 tmpfs,是一种大小灵活,直接作用在内存中的文件系统。initramfs 包含的工具和脚本,在正式的根文件系统的初始化脚本 init 启动之前,就被挂载。initramfs 是可选的,内核编译选项默认开启 initramfs(initrd)。那么什么情况下考虑使用 initramfs 呢?

  • 加载模块,如三方驱动
  • 定制化启动系统
  • 制作一个很小的 rescue shell
  • 内核不能,但是用户态可以完成的命令

initramfs 在内核启动的早期提供用一个户态环境,用于完成在内核启动阶段不易完成的工作。initramfs 包含的工具可以解密抽象层(用于加密的文件系统),逻辑卷管理器,软件 RAID,蓝牙驱动程序等。

一个 initramfs 至少包含一个文件,即 /init,内核将这个文件执行起来的进程设置为 main init 进程,pid = 1。内核挂载 initramfs 时,文件系统的根分区并没有挂载,所以无法访问文件系统中的文件。大多数嵌入式设备可能需要一个 shell,那么也会在 initramfs 打包进一个 shell。如果还需要其他工具或者脚本,也可以打包进 initramfs,但注意,必须包含依赖,因为 initramfs 是一个能够独立运行的 ram 文件系统

如何工作

initramfs 和我们常见的文件系统类似,可能存在 /usr、/bin 等目录。里面包含着我们的工具和脚本。initramfs 需要使用 cpio 来归档,cpio 是一个有着古老历史的文件归档解决方案,类似于 linux 中常用的 tar,或者 windows 中的 zip,主要作用是将多个文件打包成一个文件(但是没有压缩)。使用 cpio,是因为其代码易于实现,而且能够兼容更多的设备。

归档之后,需要考虑 initramfs 文件的体积,要进一步压缩,减少内存或磁盘的占用。所有文件,工具,库,配置设置(如果适用)等都放入 cpio 归档后,使用 gzip 实用程序压缩 cipo 文件,并将其与linux 内核一起存储。引导加载程序(通常是 grub 或者 uboot)将在引导时将其提供给内核,以便内核知道需要一个 initramfs。

内核一旦检测到 initramfs,会创建一个 tmpfs 文件系统,提取 gzip 中存档的 initramfs,并存入 tmpfs 中,内核启动 tmpfs 文件系统中的 init 脚本。该脚本用于挂载实际的根文件系统,当完成根文件系统和其他的一些重要的文件系统的安装之后,init 脚本会切换至真实的根文件系统,并在系统上调用 / sbin / init 二进制文件,进行后续的启动过程。

initramfs配置

使用make menuconfig进入内核菜单后。

配置General setup —> [*]Initial RAM filesystem and RAM disk (initramfs/initrd) support。

配置General setup —> ()Initramfs source file(s) [注]在括号里写入构建的rootfs路径,绝对路径和相对路径均可。此处填写的是busybox编译的rootfs.cpio。

make后重新编译内核即可

与ramdisk区别

ramdisk是在一块内存区域中创建的块设备,用于存放文件系统。ramdisk的容量是固定的,不能象ramfs一样动态增长。ramdisk需要内核的文件系统驱动程序(如ext2)来操作其上的数据,而ramfs则是内核的天然特性,无需额外的驱动程序。ramdisk也象其他文件系统设备一样, 需要在块设备和内存中的磁盘高速缓存之间复制数据,而这种数据复制实际不必要的

gcc编译过程

  • 预处理阶段,完成宏定义和include文件展开工作 (.i)
  • 根据编译参数进行不同程度的优化,编译成汇编代码 (.S)
  • 用汇编器把汇编代码进一步生成目标代码 (.o)
  • 用连接器把生成目标的代码和系统或者用户提供的库连接起来,生成可执行文件

gdb功能

  • 设置断点

  • 监视程序变量的值

  • 程序单步执行

  • 显示修改变量值

  • 显示修改寄存器

  • 查看程序堆栈情况

gdb调试使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
gcc -g  main.c                      //在目标文件加入源代码的信息
gdb a.out

(gdb) start //开始调试
(gdb) n //一条一条执行
(gdb) step/s //执行下一条,如果函数进入函数
(gdb) backtrace/bt //查看函数调用栈帧
(gdb) info/i locals //查看当前栈帧局部变量
(gdb) frame/f //选择栈帧,再查看局部变量
(gdb) print/p //打印变量的值
(gdb) finish //运行到当前函数返回
(gdb) set var sum=0 //修改变量值
(gdb) list/l 行号或函数名 //列出源码
(gdb) display/undisplay sum //每次停下显示变量的值/取消跟踪
(gdb) break/b 行号或函数名 //设置断点
(gdb) continue/c //连续运行
(gdb) info/i breakpoints //查看已经设置的断点
(gdb) delete breakpoints 2 //删除某个断点
(gdb) disable/enable breakpoints 3 //禁用/启用某个断点
(gdb) break 9 if sum != 0 //满足条件才激活断点
(gdb) run/r //重新从程序开头连续执行
(gdb) watch input[4] //设置观察点
(gdb) info/i watchpoints //查看设置的观察点
(gdb) x/7b input //打印存储器内容,b--每个字节一组,7--7组
(gdb) disassemble //反汇编当前函数或指定函数
(gdb) si // 一条指令一条指令调试 而 s 是一行一行代码
(gdb) info registers // 显示所有寄存器的当前值
(gdb) x/20 $esp //查看内存中开始的20个数

poll机制 使用方法

使用非阻塞IO的应用程序通常会使用select和poll系统调用查询是否可对设备进行无阻塞的访问,这两个系统调用最终会引发设备驱动中poll函数被执行。

atomic_t介绍

atomic_t: 这是一个用于定义原子变量的类型。原子变量是一种特殊的变量,可以在多线程环境下安全地进行操作,而不需要使用锁来防止竞争条件。atomic_t 通常用于存储整数值,并且提供了一组原子操作函数来保证操作的原子性。

ATOMIC_INIT(0): 这是一个宏,用于初始化原子变量。在这种情况下,ATOMIC_INIT(0)gstSub1gMsgFlag 初始化为 0。原子变量的初始化通常需要使用这个宏,而不是直接赋值,因为这样可以保证初始化过程的线程安全性。

DECLARE_WAIT_QUEUE_HEAD介绍

用于在linux内核编程中声明和初始化等待队列(wait queue)头的宏。等待队列用于在某些条件满足之前让线程处于睡眠状态,并且在条件满足的时候唤醒他们。常用于阻塞式等待

1
2
3
4
DECLARE_WAIT_QUEUE_HEAD(msg_wait)
等价于
wait_queue_head_t msg_wait;
init_waitqueue_head(&msg_wait);
wake_up_interruptible介绍

用于唤醒等待队列中处于可中断睡眠状态的进程。进程被唤醒后,会从睡眠状态恢复并执行对应任务(.poll绑定函数)。

poll_wait(file, &msg_wait, wait);介绍

将当前进程mgs_wait添加到指定的等待队列中。

poll函数返回事件的状态。

image-20240812150319868

linux设备驱动注册sys文件访问

device_create_file 在当前设备的sys目录下创建一个属性对应的文件。

设备属性文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct device_attribute {
struct attribute attr;
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};

#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

定义一个device_attribute类型的变量,##代表将两边的名字拼接起来,因此,我们得到的变量名称是含有dev_attr的,该宏定义需要 传入四个参数,name,mode,show,store分别代表文件名,文件权限,show回调函数(cat 节点),store回调函数(echo 节 点),参数mode,可以用宏(S_IRUSR、S_IWUSR、S_IXUSR等等)来定义,表示读写权限。

extern int device_create_file(struct device *device,
const struct device_attribute *entry);
extern void device_remove_file(struct device *dev,
const struct device_attribute *attr);

device表示设备,其成员中有个bus_type变量,用于指定设备挂载在某个总线上,并且会在总线的devices目录下创建一个属于该设备的目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#用于在sys文件系统中创建一个设备属性

#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>

static ssize_t my_sysfs_show(struct device *dev, struct device_attribute *attr, char *buf) {
return sprintf(buf, "Hello, world!\n");
}

static ssize_t my_sysfs_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) {
// 处理写入的数据
return count;
}

static DEVICE_ATTR(my_sysfs_file, 0664, my_sysfs_show, my_sysfs_store);

static int __init my_driver_init(void) {
struct device *dev = /* 获取设备指针 */;
int retval;

retval = device_create_file(dev, &dev_attr_my_sysfs_file);
if (retval) {
printk(KERN_ERR "Failed to create sysfs file\n");
return retval;
}

printk(KERN_INFO "Sysfs file created\n");
return 0;

}

static void __exit my_driver_exit(void) {
struct device *dev = /* 获取设备指针 */;
device_remove_file(dev, &dev_attr_my_sysfs_file);
printk(KERN_INFO "Sysfs file removed\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");

shc加密脚本

当shell脚本中包含密码,不希望其他人查看到shell脚本中的密码等信息,可以安装使用SHC工具进行加密。

Shc可以用来对shell脚本进行加密,可以将shell脚本转换为一个可执行的二进制文件。经过shc对shell脚本进行加密后,会同时生成两种个新的文件,一个是加密后的可执行的二进制文件(文件名以.x结束),另一个是C语言的原文件(文件名以.x.c结束)。

常用参数
1
2
3
4
5
-e date (指定过期日期)
-m message (指定过期提示的信息)
-f script_name(指定要编译的shell的路径及文件名)
-r Relax security. (可以相同操作系统的不同系统中执行)
-v Verbose compilation(编译的详细情况)
使用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# shc -v -f abc.sh
-v 是现实加密过程
-f 后面跟需要加密的文件
运行后会生成两个文件:
abc.sh.x 和 abc.sh.x.c
abc.sh.x为二进制文件,赋予执行权限后,可直接执行。更改名字mv abc.sh.x a.sh
abc.sh.x.c 是c源文件。基本没用,可以删除

另shc还提供了一种设定有效执行期限的方法,过期时间,如:
# shc -e 28/01/2012 -m "过期了" -f abc.sh
选项“-e”指定过期时间,格式为“日/月/年”;选项“-m”指定过期后执行此shell程序的提示信息。
如果在过期后执行,则会有如下提示:

# ./abc.sh.x
./abc.sh.x: has expired!
过期了
使用unshc可以解密shc加密的文件

padmux引脚复用

对于sstar芯片。

pad mux dtsi文件路径为kernel/arch/arm/boot/dts/xxx-padmux.dtsi。根据项目需求,修改该文件即可设置pin脚的复用模式。

image-20240808142459883

第一列为pin脚名称,第二列为pin脚复用模式,参考m_stPadMuxTbl 数组中的mode选项,第三列为复用功能中具体的pin脚作用,参考mdrv_puse.h

gpio映射表

每一个chip都有一个gpio-mapping-table.xlx文件,里面有相关gpio设置。

padmux表

每个pin脚具体的复用功能也可以见mhal_pinmux.c的m_stPadMuxTbl数组。复用关系优先级从高到低,gpio功能的优先级最低。复用某个pin脚的某个功能前要确认高优先级复用是关闭的。

image-20240808142923826

1
2
3
4
5
6
PAD_FUART_TX:        引脚
CHIPTOP_BANK: 寄存器地址
REG_SR_PCK_MODE: 偏移地址
REG_PWM0_MODE_MASK mask位
BIT1|BIT0 对应的设置bit位
PINMUX_FOR_PWM0_MODE_3: 模式
设置gpio9为gpio模式

image-20240808143130176

通过读取寄存器来确认复用是否成功

设置i2cs0为pwm模式

image-20240808143231127

image-20240808143325826

标准spi,双线,三线,四线spi

标准spi

CLK,CS,MOSI,MISO,WP,Hold。在标准spi下,spi是全双工的,MOSI,MISO分别负责不同方向的传输。大多数单个SPI串行吞吐速率达到10Mbps左右。

双线spi

CLK,CS,IO0,IO1,WP,Hold,双线spi是Dual SPI就是MOSI,MISO同时朝一个方向发数据,单词可以同时传输2bit,此时MOSI 和 MISO则被改称为IO0和IO1。在双线spi下,spi是双线半双工。串行吞吐量速率达到20Mbps左右。

三线spi

SCLK, MOSI,CS。数据收发只有一根线,适用于单工通讯,主机只发送或者只接收从机的数据。spi是半双工的。

四线spi

CLK,CS,IO0,IO1,IO2,IO3,Wp,Hold。Quad Spi,IO0,IO1,IO2,IO3被用于同一个方向传输数据。单次可传输4bit。此时MOSI和MISO为IO0,IO1,而WP,HOLD被用作IO2,IO3,CS和CLK不变。半双工40Mbps

emmc分区

emmc有默认四个物理分区。是出场即默认存在的。

image-20240807190154285

各个分区有独立的地址,都是从0x00开始的。Boot1,Boot2和RPMB的大小会在出厂的时候就设定好。两个boot的大小是完全一致的。由Extended CSD register的BOOT_SIZE_MULT Filed决定。大小的计算公式如下:

image-20240807191047225

一般情况下,Boot Area Partition 的大小都为 4 MB,即 BOOT_SIZE_MULT 为 32,部分芯片厂家会提供改写 BOOT_SIZE_MULT 的功能来改变 Boot Area Partition 的容量大小。BOOT_SIZE_MULT 最大可以为 255,即 Boot Area Partition 的最大容量大小可以为 255 x 128 KB = 32640 KB = 31.875 MB。

分区编制

具体的数据读写访问操作实际访问哪一个硬件分区。是由EMMC 的 Extended CSC register的PARTITION_CONFIG Field 中 的 Bit[2:0]: PARTITION_ACCESS 决定的,用户可以通过配置来切换硬件分区的访问。

也就是说,用户在访问特定的分区前,需要先发送命令,配置 PARTITION_ACCESS,然后再发送相关的数据访问请求。

从Boot area启动

eMMC 中定义了 Boot State,在 Power-up、HW reset 或者 SW reset 后,如果满足一定的条件,eMMC 就会进入该 State。进入 Boot State 的条件如下:

Original Boot Operation
CMD 信号保持低电平不少于 74 个时钟周期,会触发 Original Boot Operation,进入 Boot State。

Alternative Boot Operation
在 74 个时钟周期后,在 CMD 信号首次拉低或者 Host 发送 CMD1 之前,Host 发送参数为 0xFFFFFFFA 的 COM0时,会触发 Alternative Boot Operation,进入 Boot State。

在 Boot State 下,如果有配置 BOOT_ACK,eMMC 会先发送 “010” 的 ACK 包,接着 eMMC 会将最大为 128Kbytes x BOOT_SIZE_MULT 的 Boot Data 发送给 Host。传输过程中,Host 可以通过拉高 CMD 信号 (Original Boot 中),或者发送 Reset 命令 (Alternative Boot 中) 来中断 eMMC 的数据发送,完成 Boot Data 传输。

Boot Data 根据 Extended CSD register 的 PARTITION_CONFIG Field 的 Bit[5:3]:BOOT_PARTITION_ENABLE 的设定,可以从 Boot Area Partition 1、Boot Area Partition 2 或者 User Data Area 读出。

Boot Data 存储在 Boot Area 比在 User Data Area 中要更加的安全,可以减少意外修改导致系统无法启动,同时无法更新系统的情况出现。

http://www.wowotech.net/basic_tech/emmc_partitions.html

RPMB Partition

Replay Protected Memory Block Partition是emmc中一个具有安全特性的分区。eMMC 在写入数据到 RPMB 时,会校验数据的合法性,只有指定的 Host 才能够写入,同时在读数据时,也提供了签名机制,保证 Host 读取到的数据是 RPMB 内部数据,而不是攻击者伪造的数据。

RPMB 在实际应用中,通常用于存储一些有防止非法篡改需求的数据,例如手机上指纹支付相关的公钥、序列号等。RPMB 可以对写入操作进行鉴权,但是读取并不需要鉴权,任何人都可以进行读取的操作,因此存储到 RPMB 的数据通常会进行加密后再存储。

容量大小

两个 RPMB Partition 的大小是由 Extended CSD register 的 BOOT_SIZE_MULT Field 决定,大小的计算公式如下:

Size = 128Kbytes x BOOT_SIZE_MULT

一般情况下,Boot Area Partition 的大小为 4 MB,即 RPMB_SIZE_MULT 为 32,部分芯片厂家会提供改写 RPMB_SIZE_MULT 的功能来改变 RPMB Partition 的容量大小。RPMB_SIZE_MULT 最大可以为 128,即 Boot Area Partition 的最大容量大小可以为 128 x 128 KB = 16384 KB = 16 MB。

Replay Protected 原理

使用 eMMC 的产品,在产线生产时,会为每一个产品生产一个唯一的 256 bits 的 Secure Key,烧写到 eMMC 的 OTP 区域(只能烧写一次的区域),同时 Host 在安全区域中(例如:TEE)也会保留该 Secure Key。

在 eMMC 内部,还有一个RPMB Write Counter。RPMB 每进行一次合法的写入操作时,Write Counter 就会自动加一 。

通过 Secure Key 和 Write Counter 的应用,RMPB 可以实现数据读取和写入的 Replay Protect。

User Data Area

通常是EMMC分区中最大的一个。在实际产品中共,是主要区域。

容量大小

容量大小不需要设置,在配置完其他区域之后,扣除Enhanced attribute损耗的容量,剩余的就是UDA的容量。

区域属性

eMMC 标准中,支持为 UDA 中一个特定大小的区域设定 Enhanced attribute。与 GPP 中的 Enhanced attribute 相同,eMMC 标准也没有定义该区域设定 Enhanced attribute 后对 eMMC 的影响。Enhanced attribute 的具体作用,由芯片制造商定义、

  • Default, 未设定 Enhanced attribute。
  • Enhanced storage media, 设定该区域为 Enhanced storage media。

在实际的产品中,UDA 区域设定为 Enhanced storage media 后,一般是把该区域的存储介质从 MLC 改变为 SLC。通常,产品中可以将某一个 SW Partition 设定为 Enhanced storage media,以获得更好的性能和健壮性。

命令说明

mmc dev [dev] [part]

切换物理分区。mmc 设备编号,第一个为 0 part: 0 表示不访问引导分区 1 表示访问引导分区 1(boot0) 2 表示访问引导分区 2(boot1)

eg:切换到引导分区 1 –> mmc dev 0 1

mmc bootbus [dev] [boot_bus_width] [reset_boot_bus_width] [boot_mode]

设置总线位宽。

mmc partconf [dev ] [boot_ack ] [boot_partition ] [partition_access]

设置启动分区,dev 为mmc设备编号。boot_ack为是否应答。boot_partition用户选择发送到主机的引导数据。partition_access用户选择要访问的分区。

什么是netlink?

netlink提供的是内核态和用户态之间的通信机制,也可以用于用户态和用户态的两个进程之间通信。

netlink的优势

一般的用户态和内核空间通信方式有三种,ioctl,proc,netlink。而前两种都是单向的,而netlink可以实现双向通信。基于BSD socket和AF_NETLINK协议簇。使用32位的端口号寻址。每个netlink协议,通常与一个或者一组内核服务/组件相关联。NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。

netlink的特点

  • 支持全双工,异步通信
  • 在内核空间使用专门的内核api接口
  • 支持多播,可以实现总线式订阅
  • 在内核端可以用于进程上下文和中断上下文
  • 用户空间使用标准BSD Socket接口

关键数据结构

image-20240805200041276

msghdr

我们知道socket消息的发送和接收函数一般有这几对:recv/send、readv/writev、recvfrom/sendto。当然还有recvmsg/sendmsg,前面三对函数各有各的特点功能,而recvmsg/sendmsg就是要囊括前面三对的所有功能,当然还有自己特殊的用途。msghdr的前两个成员就是为了满足recvfrom/sendto的功能,中间两个成员msg_iov和msg_iovlen则是为了满足readv/writev的功能,而最后的msg_flags则是为了满足recv/send中flag的功能,剩下的msg_control和msg_controllen则是满足recvmsg/sendmsg特有的功能。

sockaddr_nl

image-20240805200415939

1
2
3
4
5
6
7
struct sockaddr_nl
{
sa_family_t nl_family; /*该字段总是为AF_NETLINK */
unsigned short nl_pad; /* 目前未用到,填充为0*/
__u32 nl_pid; /* process pid */
__u32 nl_groups; /* multicast groups mask */为0代表不希望加入任何多播组
};
struct nlmsghdr

netlink报文消息由头和消息体构成。

1
2
3
4
5
6
7
8
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message including header */ 整个消息的长度,按字节计算。包括了Netlink消息头本身。
__u16 nlmsg_type; /* Message content */ 消息的类型,数据消息还是控制消息
__u16 nlmsg_flags; /* Additional flags */ 附加的额外信息
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};

用户空间使用netlink

1.创建socket
1
2
3
4
int socket(int domain, int type, int protocol)
domain指代地址族,即AF_NETLINK;
套接字类型为SOCK_RAW或SOCK_DGRAM,因为netlink是一个面向数据报的服务;
protocol选择该套接字使用哪种netlink特征。
1
bind(fd, (struct sockaddr*)&, nladdr, sizeof(nladdr));
3.发送netlink消息

为了发送一条netlink消息到内核或者其他的用户空间进程,另外一个struct sockaddr_nl nladdr需要作为目的地址,这和使用sendmsg()发送一个UDP包是一样的。

  • 如果该消息是发送至内核的,那么nl_pid和nl_groups都置为0.
  • 如果消息是发送给另一个进程的单播消息,nl_pid是另外一个进程的pid值而nl_groups为零。
  • 如果消息是发送给一个或多个多播组的多播消息,所有的目的多播组必须bitmask必须or起来从而形成nl_groups域。sendmsg(fd, &, msg, 0);
4.接收netlink消息

一个接收程序必须分配一个足够大的内存用于保存netlink消息头和消息负载。然后其填充struct msghdr msg,再使用标准的recvmsg()函数来接收netlink消息。

当消息被正确的接收之后,nlh应该指向刚刚接收到的netlink消息的头。nladdr应该包含接收消息的目的地址,其中包括了消息发送者的pid和多播组。同时,宏NLMSG_DATA(nlh),定义在netlink.h中,返回一个指向netlink消息负载的指针。调用close(fd)关闭fd描述符所标识的socket;recvmsg(fd, &, msg, 0);

内核空间使用netlink

1
2
3
4
struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,struct module *module);
发送单播信息
1
2
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)
ssk为kernel create返回的socket,skb存放消息,data段指向netlink消息结构,skb控制块保存了消息的地址信息。pid为接收消息的进程信息。nonblock表示是否阻塞。
发送广播信息
1
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)
1
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)

设备树

0%