[Linux]
[1] 目录
net 网络协议栈及socket
fs 文件系统实现(vfs、具体文件系统)及编程接口(系统调用)
init 系统初始化核心代码,不能裁剪
kernel 系统核心代码, 进程管理,不能裁剪
ipc 进程间通信实现(消息队列、信号量和共享内存)
mm 内存管理
arch(经常需要添加代码) 架构相关代码
boot 内启动前的准备(分离内核和ramdisk、自解压)代码
kernel cpu需要的核心代码,内核真正的启动代码在head.S
mm cpu相关的内存管理代码
lib cpu相关的标准库代码
configs 支持的开发板的默认配置文件
include cpu相关的代码的头文件
plat-VENDOR 某个厂家芯片需要用到的公共代码
include
plat-SOC系列 芯片厂家的一个系列SOC的公共代码
include
mach-SOC 某个SOC用到的源代码
include
drivers(经常需要添加代码)通用设备驱动
include 头文件
scripts 编译配置脚本
Docutmentation 作者写的文档
[2] 文件
源文件
*.c/*.S/*.h 源代码
Makefile 编译规则
Kconfig 配置脚本
目标文件
.config 内核当前配置结果(选取哪些源代码)
*.o 目标文件
System.map 符号表
vmlinux ELF格式的可执行程序(带调试信息)
arch/arm/boot/Image Binaray格式可执行程序
arch/arm/boot/zImage 压缩的内核的二进制文件(带自解压算法)
[3] 特点
1. 支持多种CPU
2. 支持多种硬件驱动
任何一款嵌入式产品,都不会用到所有的源代码,所以用源码生成目标文件的步骤:
1. 选取需要的源代码 -- 配置
2. 编译选取的源代码 -- 编译
[4] 编译
1. 配置
$ cp arch/arm/configs/s5pc100 .config
$ make menuconfig
2. 编译
make
[5] 编译原理
1. 过程
见《内核编译流程》
2. 结果
见《[2] 文件》
3. 模块的Makefile
obj-y := 加入这个变量的*.o文件,被静态编译到内核(链接到zImage中)
obj-m := 加入这个变量的*.o文件, 被动态编译到内核(链接到*.ko中)
[6] 配置原理
1. 控制目录/文件是否编译
(1) 目录
例: CONFIG_SPI
在.config里面查看CONFIG_SPI配置项值:
CONFIG_SPI=y 静态选中
# CONFIG_SPI ... 没有选中
在drivers/Makefile中,查看控制目录编译的方法:
obj-$(CONFIG_SPI) += spi/
(2) 文件
例: CONFIG_SPI_MASTER
在.config里面查看CONFIG_SPI_MASTER配置项值:
CONFIG_SPI_MASTER=y 静态选中
CONFIG_SPI_MASTER=m 动态选中
# CONFIG_SPI_MASTER ... 没有选中
在drivers/spi/Makefile中,查看控制目录编译的方法:
obj-$(CONFIG_SPI_MASTER) += spi.o
2. 选中文件中的一部分代码(条件编译)
例:
#define CONFIG_DEBUG_LL (通过是否有这个宏定义来控制代码是否被编译到内核)
#ifdef CONFIG_DEBUG_LL (arch/arm/kernel/head.S)
....
#endif
在.config里面查看CONFIG_DEBUG_LL配置项值:
CONFIG_DEBUG_LL=y 静态选中
编译时,会被转义成:
#define CONFIG_DEBUG_LL 1 (include/generated/autoconf.h)
# CONFIG_DEBUG_LL=y 不选中
编译时,会被转义成:
// #define CONFIG_DEBUG_LL 1 (include/generated/autoconf.h)
3. 提供数据给源代码(宏替换)
例: CONFIG_CMDLINE
在.config中,查看CONFIG_CMDLINE配置项值:
CONFIG_CMDLINE="root=/dev/mtdblock2 rootfstype=cramfs init=/linuxrc console=ttySAC2,115200"
编译时,被转义成:
#define CONFIG_CMDLINE "root=/dev/mtdblock2 rootfstype=cramfs init=/linuxrc console=ttySAC2,115200"
4. Kconfig语法原理
1. 配置项值
<> tristate
y 静态选中
m 动态选中
n 不选中
[] bool
y 静态选中
n 不选中
() string
2. 语法解释及例子
见《Kconfig》
[7] 如何添加模块到工程中?(必须掌握)
1. 添加源代码到对应目录
例: drivers/hello/module_hello.c
2. 给子目录添加Makefile(或者直接修改)
obj-y += module_hello.o 表示静态编译到内核
obj-m += module_hello.o 表示动态编译到*.ko
obj-$(CONFIG_HELLO) += module_hello.o 根据图形菜单的配置决定静态/动态编译到内核
3. 修改上一级目录下的Makefile
obj-y += hello/
4. 添加Kconfig配置脚本
config HELLO
tristate "hello config"
help
hello config help
5. 修改上一级目录下的Kconfig
source "drivers/hello/Kconfig"
[9] 系统启动流程
[kernel]
1. 检查内核是否支持正在运行的CPU(A8)
[方法]
(1) 读取CPU内部的id
(2) 调用__lookup_processor_type子程序检查内核是否支持该CPU
(3) 如果内核不支持该CPU,打印‘P‘类型错误(Error: unrecognized/unsupported processor variant(当前运行的CPU ID))
(4) 如果支持该CPU,继续运行
错误原因: 操作系统不支持当前CPU所致,一般不会出现
解决办法: 修改操作系统(ARM公司或操作系统厂家)
2. 检查内核是否支持正在运行的主板
(1) 主板id(arch number / mach type id)存储在R1(BootLoader放置)
(2) 调用__lookup_mach_type子程序检查内核是否支持该主板
(3) 如果内核不支持该CPU,打印‘A‘类型错误
(Error: unrecognized/unsupported machine ID (r1 = BootLoader传输的主板id).
Available machine support:\n\nID (hex)\tNAME
Please check your kernel config and/or bootloader.)
(4) 如果内核支持该主板,继续运行
错误原因: 1. 操作系统确实不支持改主板,交给芯片厂家完成(概率非常小)
2. bootloader传输的id错误
解决办法: 检查BootLoader传入内核的arch number是否正确
(1) 根据主板名字到include/generated/mach-types.h文件中查出当前主板ID
(2) 对比当前主板ID和A错误中打印的R1寄存器的值
3. 循环调用驱动模块(静态加载)的初始化函数
错误原因: 驱动的初始化函数有问题,导致内核崩溃(OOPS), 崩溃驱动代码检测方法
解决办法: 见《内核调试方法》
4. 打开console终端,并且把它设置成内核的标准输入、标准输出和标准错误
错误现象: unable to open an intial console.
错误原因: /dev/console不存在(文件系统制作时必须创建)
5. 挂载根文件系统
错误现象:
Root-NFS: No NFS server available, giving up.
VFS: Unable to mount root fs via NFS, trying floppy.
VFS: Cannot open root device "mtdblock0" or unknown-block(2,0)
Please append a correct "root=" boot option; here are the available partitions:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
错误原因: (1) root=参数指定的存放文件系统的设备不存在
(2) 设备上没有存储文件系统(或文件系统类型操作系统不支持 或文件系统错误)
解决办法: (1) 检查root=参数设置的设备是否正确
(2) 设备上存储的文件系统是否正确
6. 启动init进程
错误现象: (1) Failed to execute /xxxy. Attempting defaults...
(2) NO init found. .......
错误原因: (1) init=参数指定的程序无法启动
(2) init=参数指定的程序有问题
&& /sbin/init不存在
&& /etc/init不存在
&& /bin/init不存在
&& /bin/sh不存在
解决办法: (1) 检查init=参数指定的程序是正确
(2) 检查init=参数指定的程序是正确
[rootfs]
7. /etc/inittab(init进程解析执行)
终端::动作:程序或脚本
[终端]
省略名字/dev/, 脚本之前不能有任何字符(包括空格):
例:
console::sysinit:/etc/init.d/rcS(console前面不能有任何字符,包括空格)
[动作]
sysinit 指定系统初始化阶段执行的程序或脚本
askfirst(respawn) 指定系统正常运行阶段,需要一直执行的程序或脚本
ctrlaltdel 指定在按Ctrl + Alt +Del组合键时运行的程序(只能在标准终端上起作用)
restart 指定系统重启前运行的脚本或脚本
shutdown 指定系统关闭前运行的程序或脚本
[程序或脚本]
应用程序或shell脚本
8. /etc/init.d/rcS
#!/bin/sh 脚本解析器
/bin/mount -a 挂载/etc/fstab指定的文件系统
告诉内核mdev程序的路径,系统有热插拔设备时,内核会调用mdev程序新建或删除对应设备文件节点
echo /sbin/mdev > /proc/sys/kernel/hotplug
启动mdev程序,扫描系统识别的设备,并且新建设备文件节点
/sbin/mdev -s
/etc/fstab内容
存放文件系统的设备 目录 文件系统该类型
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
tmpfs挂载时,文件系统为空,当向文件系统写入数据时,所有数据文件都存储到内存
9. /etc/profile
#!/bin/sh 脚本解析器
export HOSTNAME=farsight 机器名
export USER=root 用户名
export HOME=root 用户主目录
export PS1="[[email protected]$HOSTNAME \W]\# " 命令提示符
PATH=/bin:/sbin:/usr/bin:/usr/sbin PATH环境变量
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH 动态库的搜索路径
export PATH LD_LIBRARY_PATH
注意:本脚本被登录程序调用,但是嵌入式设备中,很多没有配置登录程序,这时希望shell脚本直接执行
本脚本(需要在inittab中,启动shell程序时,在前面加上‘-‘)
10. 必须要的设备文件节点
# sudo mknod /dev/console c(字符设备) 5(主设备号 Documentation/devices.txt) 1(次设备号)
# sudo mknod /dev/null c 1 3
[10] 移植
BSP?
描述主板硬件配置的数据结构集合,也就是说它的主要功能告诉操作系统准备的硬件配置
[11] Linux应用程序调试
1. gdb远程调试
(1) 利用开发板的交叉编译器,编译要调试的程序(编译过程中要加-g参数,-O0)
# arm-cortex_a8-linux-gnueabi-gcc hello.c -o hello -g -O0
(2) 拷贝编译出来的可执行代码到开发板的root目录
# sudo cp hello /source/rootfs/root
(3) 在交叉编译器的目录下寻找gdbserver(在开发板上能运行的程序)程序
# find ~/toolchain -depth -name gdbserver
(4) 拷贝gdbserver到开发板
# sudo cp ~/toolchain/arm-cortex_a8-linux-gnueabi/debug-root/usr/bin/gdbserver /source/rootfs/root/
(5) 在开发板上运行gdbserver程序加载要调试的程序
$ ./gdbserver 192.168.0.7(开发板ip地址):1234(端口号) hello(调试的可执行程序)
(6) 在PC上,启动gdb(在PC上运行的gdb程序,也是有交叉编译器提供)并且加载要调试的可执行程序
# arm-cortex_a8-linux-gnueabi-gdb hello(要调试的程序)
(7) 在gdb程序中,远程连接开发板gdbserver:
(gdb) target remote 192.168.0.7:1234
(8) 下断点,然后让开发板调试程序继续运行
2. 段错误调试(非常有用)
(1) 利用开发板的交叉编译器,编译要调试的程序(编译过程中要加-g参数,-O0)
# arm-cortex_a8-linux-gnueabi-gcc hello.c -o hello -g -O0
(2) 拷贝编译出来的可执行代码到开发板的root目录
# sudo cp hello /source/rootfs/root
(3) 在开发板上打开core文件限制,然后运行程序
$ ulimit -c unlimited
$ ./hello
出现段错误,产生core文件
(4) 在交叉编译器的目录下寻找gdb(在开发板上能运行的程序)程序
# find ~/toolchain -depth -name gdb
# file gdb(确保是开发板上可以运行的gdb程序)
(5) 拷贝gdb到开发板
# sudo cp ~/toolchain/arm-cortex_a8-linux-gnueabi/debug-root/usr/bin/gdb /source/rootfs/root/
(6) 在开发板上运行gdb程序解析core文件
$ ./gdb hello(带调试信息的执行程序) core(产生的core文件)
$ bt(查看详细的代码出错点)
3. 内存操作错误(有用)
核心用法: memwatch.c监控工程中,内存操作错误,没有发生段错误退出
见《实验十二》
[12] 内核调试
1. 内核汇编启动代码调试(汇编阶段)
(1) 打开MMU之前
led调试 -- 用led灯的亮灭来显示代码运行到哪个位置了
直流蜂鸣器 -- 用蜂鸣器是否响,显示代码运行到哪个位置了
UART调试(BootLoader使用的console) -- 通过打印字符来代码运行到哪个位置了
(2) 打开MMU之后,进入C程序之前的汇编调试,只能用UART
内核启动初期,只建立了1M的内存映射
注意:汇编启动代码基本不会出错
2. C代码部分
[printk](必须掌握)
1. 用法
printk("<级别>""hello\n");
[级别]
0 - 7 数字越小级别越高
[注意]
(1) 不支持浮点
(2) 打印到__log_buf
2. 相关级别
int console_printk[4] = {
DEFAULT_CONSOLE_LOGLEVEL, // console_printk[0] console级别
DEFAULT_MESSAGE_LOGLEVEL, // console_printk[1] 消息默认级别,printk没有指定级别时
MINIMUM_CONSOLE_LOGLEVEL, // console_printk[2] console最高级别
DEFAULT_CONSOLE_LOGLEVEL, // console_printk[3] 默认console级别
};
四个级别可以通过应用程序空间的/proc/sys/kernel/printk文件查看或者修改
查看命令:cat /proc/sys/kernel/printk
修改命令:echo "6 5" > /proc/sys/kernel/printk
3. 消息的级别高于console级别,从console输出
实验:
1. printk_test动态编译到内核
(1) 拷贝printk_test.c 到内核的drivers/char/
(2) 在drivers/char/Makefile前面加入, obj-m += printk_test.o
(3) # make
(4) # cp arch/arm/zImage /tftpboot
(5) # cp drivers/char/printk_test.ko /source/rootfs/lib/modules/2.6.35
2. 重启开发板
3. 加载printk_test.ko模块,观察哪些消息从console输出,然后卸载printk_test.ko
加载命令:insmod /lib/modules/2.6.35/printk_test.ko
卸载命令:rmmod printk_test
4. 修改console口级别为6,然后再次加载printk_test.ko模块,观察哪些消息从console输出
4. 消息从/proc/kmsg输出实验
1. 关闭klogd程序,重启开发板
在/etc/init.d/rcS中注释以下两行:
# klogd&
# syslogd&
2. 加载printk_test.ko模块
$ insmod /lib/modules/2.6.25/printk_test.ko
3. cat /proc/kmsg 打印内核消息
4. cat /proc/kmsg 再次打印内核消息,观察结果
5. 消息被klogd和syslogd程序读到/var/log/messeges
1. 在linux启动脚本(/etc/init.d/rcS)中,启动klogd和syslogd
脚本如下:
klogd&
syslogd&
2. 加载printk_test.ko模块
$ insmod /lib/modules/2.6.25/printk_test.ko
3. 查看/var/log/messeges内核消息
cat /var/log/messeges
或
tail /var/log/messeges
[oops](内核发生段错误)(必须掌握)
(1) 产生oops的方法,见《实验十三》
(2) oops分析
// 原因
Unable to handle kernel NULL pointer dereference at virtual address 00000000
// 页目录表及其内容
pgd = c0004000
[00000000] *pgd=00000000
// oops内部编号
Internal error: Oops: 805 [#1]
last sysfs file:
Modules linked in:
// cpu现场(寄存器值)
CPU: 0 Not tainted (2.6.35 #15)
PC is at dm9000_probe+0x20/0x8c8
LR is at platform_drv_probe+0x18/0x1c
pc : [<c020ef74>] lr : [<c015f874>] psr: a0000013
sp : cfc29ef0 ip : 00000064 fp : 00000000
r10: 00000000 r9 : 00000000 r8 : 00000000
r7 : c02c5fa8 r6 : c02c5ec8 r5 : c02c5ed0 r4 : c02c5ed0
r3 : 000000ff r2 : 00000001 r1 : 00000001 r0 : 000000ec
Flags: NzCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment kernel
Control: 10c5387d Table: 20004019 DAC: 00000017
Process swapper (pid: 1, stack limit = 0xcfc28268)
// kernel_init内核线程栈里面的数据
Stack: (0xcfc29ef0 to 0xcfc2a000)
9ee0: 00000000 cfc29f00 cfc472d0 00000000
9f00: c02c5f04 c02c5ed0 c02c5ed0 c02deeec c02deeec 00000000 00000000 00000000
9f20: 00000000 c015f874 c02c5ed0 c015e99c c02c5ed0 c02c5f04 c02deeec 00000000
9f40: 00000000 c015eaac c02deeec cfc29f58 c015ea4c c015e234 cfc01d08 cfc46bc0
9f60: c02deeec c02deeec cfce1200 c02deb60 00000000 c015db28 c02768b0 c02768b0
9f80: cfc24000 c02deeec c001f128 00000000 00000013 00000000 00000000 c015ed7c
9fa0: c001973c c001f128 00000000 00000013 00000000 c0023380 c001973c c00223a0
9fc0: c02e53c0 c02e53c0 c001f0ac c001f128 c0024e1c 00000000 00000000 00000000
9fe0: 00000000 c000857c 00000000 c00084e0 c0024e1c c0024e1c fadafb7d 7bffafdd
// 函数调用历史
[<c020ef74>] (dm9000_probe+0x20/0x8c8) from [<c015f874>] (platform_drv_probe+0x18/0x1c)
[<c015f874>] (platform_drv_probe+0x18/0x1c) from [<c015e99c>] (driver_probe_device+0xa8/0x158)
[<c015e99c>] (driver_probe_device+0xa8/0x158) from [<c015eaac>] (__driver_attach+0x60/0x84)
[<c015eaac>] (__driver_attach+0x60/0x84) from [<c015e234>] (bus_for_each_dev+0x48/0x84)
[<c015e234>] (bus_for_each_dev+0x48/0x84) from [<c015db28>] (bus_add_driver+0x98/0x214)
[<c015db28>] (bus_add_driver+0x98/0x214) from [<c015ed7c>] (driver_register+0xac/0x13c)
[<c015ed7c>] (driver_register+0xac/0x13c) from [<c0023380>] (do_one_initcall+0x58/0x1b0)
[<c0023380>] (do_one_initcall+0x58/0x1b0) from [<c000857c>] (kernel_init+0x9c/0x150)
[<c000857c>] (kernel_init+0x9c/0x150) from [<c0024e1c>] (kernel_thread_exit+0x0/0x8)
(3) 根据错误信息,找到促发oops的源码
arm-cortex_a8-linux-gnueabi-addr2line 0xc020ef74(PC值,地址) -e vmlinux(带调试信息的ELF可执行文件) -f
(4) 分析错误
(1) 促发oops的源代码不一定是产生错误的源代码
(2) 如果不是,可以参考函数调用历史,回溯查找前面的函数看代码(特别是自己写的代码)是否有问题
(3) 更改的代码和促发错误的代码的关系
(4) 认真查看更改代码
[panic]
panic()函数打印消息,并挂死