今天我又犯了一个只有古法编码程序员才会犯的错误。
由于特殊情况,我需要写一个临时升级程序放到 SD 卡中,给已生产完但尚未出货的产品升级。常规 OTA 不会升级 U-Boot,这次特殊原因需要升级 U-Boot,所以要单独处理。本来只是在原升级进程里新增一个接口函数,去识别 SD 卡标志并执行升级。代码逻辑不复杂:申请一块缓冲区、读文件、处理。编译也没报错,结果一跑,memset 直接卡死。
一开始我以为是内存不够、文件有问题、指针野了、杀原进程资源释放问题等。排查一圈后,把那段程序抽出来放到虚拟机里做最小功能测试,发现同样在 memset 处出现段错误,但不是卡死。

我把 memset 注释掉,read 直接返回 -1。因为是给 SD 卡拷机程序自测用,错误处理没写那么详细。为了明确错误,加上 perror,最终错误值是 Bad address。当局者迷,还是请个“旁观者”来查代码:把最小代码和错误码丢给 AI,它一眼就发现了我的低级错误。写代码时一个疏忽,把:
char *pBuffer = NULL;
写成了:
char pBuffer = NULL;
就少打一个星号。这一个星号,够我喝一壶的。
错误现场
错误代码大概长这样,下面代码简化了,返回值判断都去掉了:
char pBuffer = NULL; // 手滑:少写了一个 *
pBuffer = (char*)malloc(fileSize); // 分配成功,但地址被截断了
memset(pBuffer, 0, fileSize); // 先卡死在这
read(fileId, pBuffer, fileSize); // 删掉 memset 后,又 Bad address
pBuffer 被定义成 char,只有 1 个字节,却想装下一个 64 位地址,装不下。
为什么 malloc 分配内存“没报错”
一开始我很奇怪:malloc 返回了非空指针,说明堆分配成功,怎么后面就崩了?
malloc 是库函数,它内部通过 brk 或 mmap 向内核申请虚拟内存,只要堆够用,返回的地址,比如 0x7f8a4b2000a0,一定是有效的。
问题出在赋值:
pBuffer = (char*)malloc(fileSize);
pBuffer 只有 1 个字节,编译器只把寄存器里的低 8 位取出来存进去。0x7f8a4b2000a0 存进 pBuffer,就只剩一个 0xa0。
这一步为什么没崩?因为赋值就是一条 MOV 指令,纯逻辑操作,CPU 不做内存地址合法性检查。我把一个大数塞进一个小盒子,操作系统管不着,这是 C 语言对程序员的信任。
为什么 read 会报 Bad address
执行 read(fileId, pBuffer, fileSize) 的时候,性质就变了:
- 系统调用把
pBuffer 的值 0xa0 作为目标地址传给内核;
- 内核的
read 实现,fs/read_write.c,要往用户态内存写数据,写之前必须调 access_ok() 检查这个地址在不在当前进程的虚拟地址空间 VMA 里;
0xa0 在内核保留区附近,0x0000 ~ 0x1000 属于不可访问区域,根本没映射到进程的堆或栈;
- 校验不过,内核直接返回
-EFAULT,也就是 Bad address,压根不会发起 DMA 去搬数据。
那为什么不是段错误?
用户态访问非法地址,MMU 会触发缺页异常,内核发 SIGSEGV 把进程杀掉,那是段错误。但这里不一样:内核在动手写之前就发现地址不对,主动返回了错误码,根本没触发硬件缺页异常,所以进程还活着,只是 read 返回了 -1。
修复就一行
char *pBuffer = NULL; // 对,就多了这一个星号
| 错误写法 |
正确写法 |
char pBuffer = NULL; |
char *pBuffer = NULL; |
| 1 个字节,地址被截断 |
8 个字节,64 位,完整保存地址 |
read 把 0xa0 传给内核 |
read 把 0x7f8a4b2000a0 传给内核 |
内核 access_ok 校验失败 → -EFAULT |
校验通过,正常填充数据 |
本来一行警告就能拦住
如果编译时开了 -Wall,GCC 会直接明说:
warning: assignment to 'char' from 'char *' makes integer from pointer without a cast [-Wint-conversion]
warning: comparison between pointer and integer ('char' and 'void *') [-Wpointer-to-int-cast]
GCC 早就看出来了,是我没开或者开了也没去看 warning。
嵌入式/Linux 的 C 项目,-Wall -Wextra -Werror 真的建议常年开着,把警告当错误。这种低级手滑,编译阶段就能拦住,根本轮不到跑起来崩。
总结
这次事故其实就是三句话:
char 由硬件固定为 1 字节,指针长度由平台决定,64 位下是 8 字节。
- C 允许隐式类型转换,但短的容器装长地址,高位必然被截掉。
- 用户态库函数只管算、不查地址合法性;系统调用要跨用户态/内核态搬数据,必须严格校验地址。
💗 解决方案:一个星号,从编译绕到运行,绕了这么大一圈,最后栽在最基础的类型上。写代码这事儿,有时候真不是技术有多深,就是手别抖,错误信息加全,尤其是 perror 这类可以明确错误的,不要只打印一个 ret。
如果你也常在嵌入式 C 项目里排查这类底层问题,欢迎到 云栈社区 分享你的调试记录。