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 上为什么能工作?
- 数据大小相同 :
unsigned int和unsigned long都是 4 字节。 - 内存布局 :
struct dma_config在 ARM32 上是连续的 8 字节:[status_code (4B)] [user_buffer (4B)]。 - 写入操作 :
*((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 long和void*(指针) 都变成了 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; 时:
&cfg.status_code获取status_code的起始地址。- 这个地址被强制转换为
unsigned long*(64位指针)。 - 编译器生成指令,向这个地址写入一个 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 : 12345678b7f12000和x3 : 12345678b7f12000: 寄存器x1和x3中存储着那个非法地址。根据 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 指针。