linux系统移植过程中挂死问题分析

1,场景分析

设备有一个硬件寄存器,用于存储一个 32 位的状态码。我们编写了一个函数来读取这个状态码,现在需要将这部分代码运行在aarch64位系统上,但是移植后会出现系统挂死的问题。

第一步:在 ARM32 上"正常"运行的代码

首先,是共享给用户空间的头文件和驱动代码。

复制代码
// my_dma.h - 用户空间和内核空间共用
struct dma_config {
    unsigned int status_code; // 4字节,用于返回操作状态
    void* user_buffer;        // 在ARM32上是4字节指针,指向用户缓冲区
};

// my_dma_driver.c - 内核驱动代码
#include "my_dma.h"

long my_dma_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct dma_config cfg;

    // 从用户空间复制结构体
    if (copy_from_user(&cfg, (void __user *)arg, sizeof(cfg))) {
        return -EFAULT;
    }

    // ... 启动DMA硬件 ...

    // *** BUGGY CODE START ***
    // DMA完成后,硬件返回一个64位的状态码(虽然ARM32只有32位寄存器)
    // 程序员想把这个状态码写入status_code,他错误地认为应该按64位写入
    // 他没有创建新指针,而是直接对结构体成员地址进行类型转换
    *((unsigned long*)&cfg.status_code) = 0x12345678; // 在ARM32上,这实际是32位写入
    // *** BUGGY CODE END ***

    // 将更新后的结构体(包含状态码)复制回用户空间
    if (copy_to_user((void __user *)arg, &cfg, sizeof(cfg))) {
        return -EFAULT;
    }

    return 0;
}

在 ARM32 上为什么能工作?

  1. 数据大小相同unsigned intunsigned long 都是 4 字节。
  2. 内存布局struct dma_config 在 ARM32 上是连续的 8 字节:[status_code (4B)] [user_buffer (4B)]
  3. 写入操作*((unsigned long*)&cfg.status_code) 这行代码,虽然意图是按 unsigned long 写入,但在 ARM32 上,unsigned long 就是 4 字节。所以它只会向 cfg.status_code 写入 4 字节数据,没有越界。cfg.user_buffer 的值保持不变,程序一切正常。

第二步:移植到 AArch64 时的灾难

现在,我们将代码移植到 AArch64。数据模型变为 LP64:

  • unsigned int 仍然是 32 位 (4 字节)
  • unsigned longvoid* (指针) 都变成了 64 位 (8 字节)

1. 内存布局的变化

struct dma_config 的内存布局在 AArch64 上变为:

  • status_code 是 4 字节。
  • user_buffer 指针是 8 字节。
  • 为了对齐 8 字节的 user_buffer,编译器会在 status_code 后面插入 4 字节的填充

内存布局:[status_code (4B)] [padding (4B)] [user_buffer (8B)]。总大小从 8 字节变为 16 字节。

2. 致命的写入操作

my_dma_ioctl 在 AArch64 上执行 *((unsigned long*)&cfg.status_code) = 0x12345678; 时:

  1. &cfg.status_code 获取 status_code 的起始地址。
  2. 这个地址被强制转换为 unsigned long*(64位指针)。
  3. 编译器生成指令,向这个地址写入一个 64 位(8 字节) 的数据。

3. 内存越界与指针破坏

这次写入操作会覆盖以下内存区域:

  • status_code 的 4 字节空间。
  • 紧随其后的 4 字节填充区。
  • user_buffer 指针的高 4 字节!

user_buffer 是一个 8 字节的指针,原本存储着用户空间传来的有效地址(例如 0x0000ffffb7f12000)。现在,它的高 32 位被 0x12345678 覆盖,变成了一个完全无效的内核空间地址(例如 0x12345678b7f12000)。

4. 系统挂死与日志分析

崩溃发生在代码的最后一步:

复制代码
// 将更新后的结构体(包含状态码)复制回用户空间
if (copy_to_user((void __user *)arg, &cfg, sizeof(cfg))) {
    // ...
}

copy_to_user 尝试将内核中的 cfg 结构体复制回用户空间时,它需要访问 arg 指向的用户空间内存。但是,arg 本身是一个有效的用户空间地址,问题不在这里。真正的崩溃点在于 copy_to_user 内部对 cfg 结构体的处理 。更常见的崩溃场景是,如果驱动程序接下来需要使用 cfg.user_buffer 来进行实际的 DMA 或数据传输:

复制代码
// 假设在 copy_to_user 之前,还有一步操作
dma_start(cfg.user_buffer, ...); // 使用被破坏的指针

当驱动程序尝试使用这个被破坏的 cfg.user_buffer 指针作为 DMA 地址时,硬件会尝试访问一个无效的、高位的内核地址,这会立即触发一个总线错误或内存访问违例。

典型的内核挂死日志如下:

复制代码
[  123.456789] Unable to handle kernel paging request at virtual address 12345678b7f12000
[  123.456790] Mem abort info:
[  123.456791]   ESR = 0x96000044
[  123.456792]   EC = 0x25 (DABT, current EL), IL = 32 bits
[  123.456793]   SET = 0, FnV = 0
[  123.456794]   EA = 0, S1PTW = 0
[  123.456795] Data abort reason:
[  123.456796]   Translation fault (level 3)
[  123.456797] swapper pgtable: 4k pages, 48-bit VAs, pgdp = 00000000401aa000
[  123.456798] [12345678b7f12000] pgd=0000000000000000, p4d=0000000000000000, pud=0000000000000000
[  123.456799] Internal error: Oops: 96000044 [#1] SMP
[  123.456800] Modules linked in: my_dma_driver(O)
[  123.456801] CPU: 1 PID: 256 Comm: my_test_app Tainted: G           O      5.15.0 #1
[  123.456802] Hardware name: My Company My Board (DT)
[  123.456803] pstate: 60000005 (nZCv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[  123.456804] pc : dma_start+0x20/0x100 [my_dma_driver]
[  123.456805] lr : my_dma_ioctl+0xc8/0x150 [my_dma_driver]
[  123.456806] sp : ffff80000c03bca0
[  123.456807] x29: ffff80000c03bca0 x28: 0000000000000000
[  123.456808] x27: ffff80000c03bd80 x26: 0000000000000001
[  123.456809] x25: 0000000000000000 x24: ffff000008014b10
[  123.456810] x23: 12345678b7f12000 x22: 0000000000000010
[  123.456811] x21: ffff80000c03bd48 x20: 0000000000000000
[  123.456812] x19: ffff80000c03bd48 x18: 0000000000000000
[  123.456813] x17: 0000000000000000 x16: 0000000000000000
[  123.456814] x15: 0000000000000000 x14: 72657474656d5f6d
[  123.456815] x13: 61726974736e695f x12: 0000000000000008
[  123.456816] x11: 0000000000000001 x10: ffff0000080816c8
[  123.456817] x9 : ffff80000c03bd48 x8 : 000000000017ffe8
[  123.456818] x7 : 0000000000000000 x6 : 0000000000000000
[  123.456819] x5 : 0000000000000000 x4 : 0000000000000000
[  123.456820] x3 : 12345678b7f12000 x2 : 0000000000000010
[  123.456821] x1 : 12345678b7f12000 x0 : ffff000008014b10
[  123.456822] Call trace:
[  123.456823]  dma_start+0x20/0x100 [my_dma_driver]
[  123.456824]  my_dma_ioctl+0xc8/0x150 [my_dma_driver]
[  123.456825]  __arm64_sys_ioctl+0xa0/0xf0
[  123.456826]  invoke_syscall+0x50/0x120
[  123.456827]  el0_svc_common.constprop.0+0xc4/0xe0
[  123.456828]  do_el0_svc+0x24/0x90
[  123.456829]  el0_svc+0x20/0x50
[  123.456830]  el0t_64_sync_handler+0x84/0x100
[  123.456831]  el0t_64_sync+0x180/0x184
[  123.456832] Code: d503201f f9400bf3 a9be7bfd 910003fd (f9400000)
[  123.456833] ---[ end trace 8a5b8c2c7a3c7a3c ]---
[  123.456834] Kernel panic - not syncing: Fatal exception
[  123.456835] SMP: stopping secondary CPUs
[  123.456836] Kernel Offset: disabled
[  123.456837] ---[ end Kernel panic - not syncing: Fatal exception ]---

日志分析:

  • Unable to handle kernel paging request at virtual address 12345678b7f12000 : 这是核心错误。CPU 尝试访问一个无效的虚拟地址 12345678b7f12000。这个地址明显是一个内核地址(高位),但不在任何有效的映射区域。
  • pc : dma_start+0x20/0x100 [my_dma_driver] : 程序计数器(PC)指向 dma_start 函数内部,说明崩溃发生在这里。
  • lr : my_dma_ioctl+0xc8/0x150 [my_dma_driver] : 链接寄存器(LR)指向 my_dma_ioctl 函数中调用 dma_start 之后的位置。
  • x1 : 12345678b7f12000x3 : 12345678b7f12000 : 寄存器 x1x3 中存储着那个非法地址。根据 AArch64 的调用约定,x0-x7 用于传递参数,这极有可能是传递给 dma_start 函数的 user_buffer 参数。
  • Call trace : 清晰地展示了调用链:my_dma_ioctl -> dma_start -> 崩溃。

总结与修正

这个例子展示了结构体内部类型不匹配的隐蔽性和破坏性。它不是通过一个显式的临时指针赋值,而是通过就地类型转换,在结构体上"开了一个洞",破坏了相邻的指针数据。

修正方法依然是使用固定宽度类型:

复制代码
// my_dma.h - 修正后的头文件
#include <stdint.h>

struct dma_config {
    uint32_t status_code;   // 明确为 32 位
    void* user_buffer;      // 指针大小由平台决定
};

// my_dma_driver.c - 修正后的驱动代码
#include "my_dma.h"

long my_dma_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct dma_config cfg;
    // ... copy_from_user ...

    // 直接操作,类型明确,安全可靠
    cfg.status_code = 0x12345678; // 即使硬件返回64位,也只取低32位或做其他处理

    // ... copy_to_user ...
    return 0;
}

通过使用 uint32_t,我们消除了所有歧义,确保无论在 32 位还是 64 位平台上,对 status_code 的写入操作都只会影响它自己的 4 字节空间,从而保护了相邻的 user_buffer 指针。

相关推荐
工藤学编程11 小时前
深入Rust:Tokio多线程调度架构的原理、实践与性能优化
性能优化·架构·rust
武子康13 小时前
Java-165 Neo4j 图论详解 欧拉路径与欧拉回路 10 分钟跑通:Python NetworkX 判定实战
java·数据库·性能优化·系统架构·nosql·neo4j·图论
前端小咸鱼一条21 小时前
16.React性能优化SCU
前端·react.js·性能优化
TDengine (老段)21 小时前
益和热力性能优化实践:从 SQL Server 到 TDengine 时序数据库,写入快 20 秒、查询提速 5 倍
大数据·数据库·物联网·性能优化·时序数据库·tdengine·1024程序员节
敲代码的猴先生1 天前
技术分享 | torch.profiler:利用探针收集模型执行信息的性能分析工具
人工智能·pytorch·经验分享·语言模型·性能优化
武子康1 天前
Java-163 MongoDB 生产安全加固实战:10 分钟完成认证、最小权限、角色详解
java·数据库·分布式·mongodb·性能优化·系统架构·nosql
小白学大数据2 天前
从携程爬取的杭州酒店数据中提取价格、评分与评论的关键信息
爬虫·python·性能优化
小刘鸭地下城2 天前
UV、PV、P95:三大核心业务指标的全维度解析
前端·性能优化
武子康2 天前
Java-164 MongoDB 认证与权限实战:单实例与分片集群 整体认证配置实战 最小化授权/错误速查/回滚剧本
java·数据库·分布式·mongodb·性能优化·系统架构·nosql