Linux驱动的调试
调试 Linux 驱动
喜欢我 O2 优化吗😤
环境配置
开始调试之前需要先根据上一篇笔记中的步骤搭建出一套在 qemu 中运行的从源码编译得到的 Linux 系统, 这之后是安装和编写调试使用的脚本:
Linux 调试用选项的开启
需要在 Linux 的 menuconfig 中打开或关闭某些妨碍调试进行的选项:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
- 
开启 kernel debuging, 该选项开启后之后的配置才能使用
Kernel hacking --> Kernel debugging [y] - 
生成 gdb 调试用 script, 该 script 会在 vmlinux 被加载进 gdb 的时候辅助使用
Kernel hacking --> Compile-time checks and compiler options --> Provide GDB scripts for kernel debugging [y] - 
禁用将调试信息存放至 .dwo 文件, 默认情况下为了节省带调试信息的文件占用空间的大小, 会将调试信息存放在另外的文件中, 这种操作给调试内核造成了不必要的麻烦, 在内存空间足够大的情况下, 禁用这个选项
Kernel hacking --> Compile-time checks and compiler options --> Produce split debuginfo in .dwo files [ ] - 
禁用文件压缩, 禁用节省空间的操作, 我的内存有很多, 不必要在这里节省空间
Kernel hacking --> Compile-time checks and compiler options --> Reduce debugging information [ ] --> Compressed Debug information (Don't compress debug information) - 
开启 KGDB 我并不知道这个有什么作用, 能开就开
Kernel hacking --> Compile-time checks and compiler options --> Generic Kernel Debugging Instruments --> KGDB: kernel debugger 
将驱动合并进 Linux Kernel 的编译
整体文件路径:
driver/net/wireless
    --> ath/            高通无线网卡驱动代码
    --> intel/          英特尔无线网卡驱动
    --> wirelesssimu/   该路径下有我的驱动代码
            xxxx.c          驱动代码
            xxxx.h          驱动代码
            Makefile        make 配置文件
            Kconfig         适配 Linux Kernel 的 Kconfig 系统
    Makefile    上层的 make 配置文件
    Kconfig     上层的 Linux Kernel Kconfig 系统的配置
因为 insmod 的方式去调试驱动过于麻烦, 因此不如直接将驱动编译进 Linux Kernel 之中, 这样就可以按照调试 Kernel 的方式来对驱动进行调试。
将驱动代码合并进 Linux Kernel 需要在驱动的路径下添加 Makefile Kconfig 文件, 并修改上级模块的 Makefile Kconfig 文件, 才能让 Linux Kernel 的编译系统认知到第三方驱动

驱动路径下的 makefile 文件, 可以看到编译方式(y/m/n)是被 CONFIG_WIRELESS_SIMU_PCI 选项控制的, 而且在Makefile中没有明确的编译命令; 添加 -Og 选项用于调试; 不想要编译的时候, 直接将该文件(Makefile)中的所有代码注释掉即可
obj-$(CONFIG_WIRELESS_SIMU_PCI) += wirelesssimu.o
wirelesssimu-y += \
		wireless.o \
		wireless_dma.o \
		wireless_mac80211.o \
		wireless_dptx.o \
		wireless_dprx.o \
		wireless_wmi.o \
		wireless_htc.o \
		wireless_ce.o \
		wireless_hal.o \
		wireless_hif.o \
		wireless_irq.o
ccflags-y += -DDEBUG -Og
驱动路径下的 Kconfig 文件: 在该文件中可以看到 config 的名称是 WIRELESS_SIMU_PCI 这种命名会被自动格式化为 Makefile 中的 CONFIG_WIRELESS_SIMU_PCI
- 
depends : 表示该选项的开启需要一定的前置条件, 一般是该驱动依赖或调用的上级模块
 - 
select : 该选项开启后会顺带开启某些选项
 - 
help :一些描述语句, 可以在 menuconfig 中使用 help 选项访问到
 
config WIRELESS_SIMU_PCI
    bool "Wireless simu pci device"
    depends on MAC80211 && HAS_DMA
    select CRYPTO_MICHAEL_MIC
    help
        This module help support for Qemu wireless firmware
上级路径中的 Makefile 文件, 在结尾添加一条代码即可, 这行代码可以让高层的make指令读取到自己的驱动路径
obj-$(CONFIG_WLAN) += wirelesssimu/
上级路径中的 Kconfig 文件, 在 endif 之前添加一行代码, 让高层的 Kconfig 认知到我驱动中的 Kconfig 文件
source "drivers/net/wireless/wirelesssimu/Kconfig"
endif # WLAN
上述步骤做完之后就可以在 menuconfig 之中看到自己的驱动配置项, 按 y 开启该配置项即可

vscode 调试用脚本
调试时需要准备两个脚本,一个用于启动 gdb 工具来进行调试, 一个用于启动 qemu 来运行 Linux Kernel 的二进制文件, 这两个脚本的位置存放在 .vscode 路径下
qemu_simudevice\                    qemu 源码
    --> build\                      qemu 的编译结果
            qemu-system-aarch64     仿真器
script\                             一些杂项
        aarchrootfs.etx3            启动 Linux 时使用的 root 文件系统
linux-stable\
    --> .vscode\                    vscode 使用的脚本
            tasks.json              启动 qemu 脚本
            launch.json             启动 gdb 脚本
    --> drivers\                    驱动
启动 qemu 的 tasks.json
- 
-s -S : 这两个参数用于在启动 qemu 后, 运行 Linux Kernel 前等待 gdb 的链接, gdb 链接完成后 Linux Kernel 继续运行
 - 
nokaslr : 禁用内核起始地址的随机化, KASLR 会将编译时的指令地址在运行时添加一个随机的偏移。由于 arm64 linux 支持内核地址的随机化, 导致在调试的时候无法正确的读取到指令在运行时的地址;在调试的时候就不必要启动这样的安全功能了
 - 
-m 16G : 由于携带符号表的内核镜像可能会很大, 而且 pci 设备需要一段较大的空间来进行数据交换, ring 的配置等等, 为虚拟机尽量分配较大的内存空间
 
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",
            "command": "${workspaceFolder}/../qemu_simudevice/build/qemu-system-aarch64",
            "args": [
                "-s",
                "-S",
                "-nographic",
                "-M",
                "virt",
                "-cpu",
                "cortex-a53",
                "-m",
                "16G",
                "-kernel",
                "${workspaceFolder}/arch/arm64/boot/Image",
                "-append",
                "nokaslr console=ttyAMA0 root=/dev/vda rw",
                "-device",
                "virtio-blk-device,drive=disk",
                "-drive",
                "file=${workspaceFolder}/../script/aarchrootfs.etx3,if=none,format=raw,id=disk",
                "-device",
                "edu",
                "-device",
                "wirelesssimu"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}
启动 gdb 的 launch.json
gdb 调试代码有 本机 attach 和 remote 三种调试方式, 在这里使用 remote 的方式链接到 qemu 自带的 gdb server进行代码的调试
- 
127.0.0.1:1234 : qemu 暴露的 gdb server 的地址和端口号
 - 
/usr/bin/gdb-multiarch : 调试非本机架构的gdb程序, 由于本机是 x86 架构的系统, 但调试的是 arm64 的操作系统, 所以这里使用 gdb-multiarch 来进行调试, 可以使用 apt install 安装
 - 
setupCommands : 保证 gdb 链接到 qemu 之后, Linux Kernel 不至于跑飞, 正常情况下应该跑到断点处就等待调试器发出下一条信息
 
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "vmlinux",
            "type": "cppdbg",
            "request": "launch",
            "miDebuggerServerAddress": "127.0.0.1:1234",
            "program": "${workspaceRoot}/vmlinux",
            "args": [],
            "stopAtEntry": true,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "/usr/bin/gdb-multiarch",
            "setupCommands": [
                {
                    "description": "stop at main function",
                    "ignoreFailures": true,
                    "text": "break start_kernel"
                }
            ]
        }
    ]
}
开始调试
- 
vscode 的 terminal --> run Task 之后在中间弹出的窗口中选择 build

 - 
在文件中打断点
 - 
按 F5 或点击 vscode 中的 Run --> start Debugging

 
qemu 设备端对 aarch 64 的适配修改
之前可以在 x86 qemu 上可以正确运行的设备模型, 放在 arm64 仿真器上发现出现了很多错误, 包括以下几点
- 
pci 的 bar 无法正确初始化。经过测试修改, 发现 arm64 不支持过大的 bar 大小, 将设备模型中的 bar 减小大小即可, 256 MB 也有 28 bit长, 足够进行 mmio 的寄存器读写, 之后再不够大可以考虑再开一个 bar
memory_region_init_io(&wd->mmio, OBJECT(wd), &wireless_simu_mmio_ops, pci_dev, "wireless_simu_mmio", (256 * MiB)); - 
中断, 在 x86 qemu 上的中断写的比较简单, 但是 arm64 对中断的触发和清除要求的更加严格, 为了保证中断不被同时触发, 要求触发中断的模块持有 bql 锁, 否则禁止触发中断。释放中断暂时没有这个要求
bql_lock(); pci_set_irq(ws_irq->pci_dev, 1); bql_unlock(); 
禁用某些函数的优化
Linux 编译时默认开启 O2 优化,而且只给了 O2 或更高的选项, 正常情况下是无法开启更低的优化的, Linux 中的很多代码在编写的过程中也是默认以 O2 优化来进行开发。如果强制去修改 Makefile 文件去将某些代码的编译修改为 Og 可能会导致不可预料的问题。
关于 O2 编译通过但 O0 则编译失败的案例, 可以参考以下的代码: 该代码可以在 userspace 使用 gcc 编译测试
/* 
 * 这段代码的作用是在编译时检查某个数是否是 0
 * 
 * 这段代码使用 gcc -O2 optTest.c -o optTest.out 编译的时候不会出错
 * 但使用 gcc -O0 optTest.c -o optTest.out 编译时会出错
 *
 * 关键点在于内联函数的参数是否被修改为了常量, 然后进行计算?最终编译器判断跳过了 __bad_mask();
 * 但是在 O0 下就会去检查 __bad_mask(); 导致报错
 */
#include<stdint.h>
#define __compiletime_error(msg) __attribute__((__error__(msg)))
extern void __compiletime_error("不等于0") __bad_mask(void);
// 该函数来自 Linux kernel 的 include/linux/bitfield.h    static __always_inline u64 field_multiplier(u64 field)
static __always_inline uint64_t field_multiplier(uint64_t field)
{
    if (field == 0)
		__bad_mask();
	return field;
}
#define TESTMASKDATA 0x00010
int main(int argc, char* argv[], char** env){
    field_multiplier(TESTMASKDATA);
}
上述代码的功能是在编译期对某个值的合法性进行判断, 如果不合法则编译不通过。这种在编译期进行判断的行为在 Kernel 中有很多, 常见于判断是否为 2 的幂, 是否是一个掩码等等。在 O2 优化下会自动的进行常量传播和运算, 合法的值不会进入错误分支, 也就不会在编译期报错。但 O0 情况下由于会将所有的条件分支的函数跑一遍, 在遇到 __bad_mask 函数的时候就直接报错
因此在调试程序的时候, 可以采用一种更精细的控制策略, 如下述代码所示, 仅将某个函数的编译设置为 Og 或 O1 这样就能大概率保证不出现编译错误;注意是大概率, 也不排除某些函数就是要开优化。
int ieee80211_register_hw(struct ieee80211_hw *hw) __attribute__((optimize("Og")));
int ieee80211_register_hw(struct ieee80211_hw *hw)
{
    // xxxxxx
}
                    
                
                
            
        
浙公网安备 33010602011771号