C 内存对齐踩坑记录

概要

本文记述了一个 C 语言使用过程中由于不当设置内存对齐属性导致的问题。

背景

笔者在多年前开发了一个模块 hm ,该模块被多个模块深度使用并一直健康稳定运行,且该模块已经两年多没有任何变动。

然而,最近一个模块使用 hm 时,发现其初始化时会导致进程 coredump ------ 而 coredump 的位置就在 hm 这个多年未动的代码上面。

问题表现

分析过程中,涉及代码实现部分均已简化删减,只保留了能说明情况的数据结构和编码。

hm 的结构体如下,该结构体由于业务需要,设置了特定的内存对齐方式:

C 复制代码
typedef struct {
    bool a;
    bool b;
    int c __attribute__((aligned(64)));
} hm_t __attribute__((aligned(128)));

其中,模块 a 的使用方法如下:

C 复制代码
int main(void)
{
    init();
    hm_var.a = true;
    hm_var.b = false;
    hm_var.c = 10086;

    HELLO("module a");
    return check(&hm_var, true, false, 10086) ? 0 : -1;
}

而模块 b 的使用方法也完全相同:

C 复制代码
int main(void)
{
    init();
    hm_var.a = true;
    hm_var.b = false;
    hm_var.c = 10086;

    HELLO("module b");
    return check(&hm_var, true, false, 10086) ? 0 : -1;
}

然而,在实际运行时,两者却产生了截然不同的效果:

shell 复制代码
focksor@focksor:~/workSpace/notebook_demo__c_param_pack_issue$ make a
gcc -std=c11 -Wall -Wextra -O2 -o module_a.elf hm.c module_a.c
./module_a.elf
hello from module a
hm var: a=1 b=0 c=10086
check: a=1 b=0 c=10086
focksor@focksor:~/workSpace/notebook_demo__c_param_pack_issue$ make b
gcc -std=c11 -Wall -Wextra -O2 -o module_b.elf hm.c module_b.c
./module_b.elf
hello from module b
hm var: a=1 b=0 c=286331153
check: a=1 b=0 c=10086
expect hm->c is 10086 but actually is 286331153
make: *** [Makefile:7: b] Error 255

可以看到,两者的使用方法完全一致,但是产生了截然不同的效果------虽然在看此文章的读者可能已经猜到了是内存对齐的问题,但是这个项目实际具有数万行的关联代码,笔者当时可是头疼得很。

分析过程

预处理

使用 gcc -E 指令预处理模块的 .c 文件,查看展开后的内容,两个模块的展开 diff 如下:

diff 复制代码
1c1
< # 0 "module_a.c"
---
> # 0 "module_b.c"
6c6,12
< # 1 "module_a.c"
---
> # 1 "module_b.c"
> # 1 "some_other_utils.h" 1
> #pragma pack(1)
> typedef struct {
>     int a;
> } some_other_struct_t;
> # 2 "module_b.c" 2
577,583c583
< # 2 "module_a.c" 2
< # 1 "some_other_utils.h" 1
< #pragma pack(1)
< typedef struct {
<     int a;
< } some_other_struct_t;
< # 3 "module_a.c" 2
---
> # 3 "module_b.c" 2
589c589
< # 7 "module_a.c" 3 4
---
> # 7 "module_b.c" 3 4
591c591
< # 7 "module_a.c"
---
> # 7 "module_b.c"
594c594
< # 8 "module_a.c" 3 4
---
> # 8 "module_b.c" 3 4
596c596
< # 8 "module_a.c"
---
> # 8 "module_b.c"
600c600
<     printf("hello from %s\n", "module a");
---
>     printf("hello from %s\n", "module b");
602c602
< # 12 "module_a.c" 3 4
---
> # 12 "module_b.c" 3 4
604c604
< # 12 "module_a.c"
---
> # 12 "module_b.c"
606c606
< # 12 "module_a.c" 3 4
---
> # 12 "module_b.c" 3 4
608c608
< # 12 "module_a.c"
---
> # 12 "module_b.c"

注意到中间有一段不同:

diff 复制代码
> # 1 "module_b.c"
> # 1 "some_other_utils.h" 1
> #pragma pack(1)
> typedef struct {
>     int a;
> } some_other_struct_t;
> # 2 "module_b.c" 2
577,583c583
< # 2 "module_a.c" 2
< # 1 "some_other_utils.h" 1
< #pragma pack(1)
< typedef struct {
<     int a;
< } some_other_struct_t;
< # 3 "module_a.c" 2

可以看到是模块引用的另外一个头文件中,使用 #pragma pack 设置了内存对齐属性(并且没有恢复!)------而如果这个在 hm.h 之前定义,将会使 hm_t 的内存对齐属性失效。

源码对比

知道了是因为内存对齐属性导致的问题,我们再回来看两个模块的源码,可以看到一些端倪:

diff 复制代码
$ diff module_a.c module_b.c  -u
--- module_a.c  2025-09-03 16:29:27.837430915 +0800
+++ module_b.c  2025-09-03 16:29:49.329691060 +0800
@@ -1,5 +1,5 @@
-#include "hm.h"
 #include "some_other_utils.h"
+#include "hm.h"
 
 int main(void)
 {
@@ -8,6 +8,6 @@
     hm_var.b = false;
     hm_var.c = 10086;
 
-    HELLO("module a");
+    HELLO("module b");
     return check(&hm_var, true, false, 10086) ? 0 : -1;
 }

可以看到,两个模块导入 some_other_utils.h 和 hm.h 的顺序不同,而 some_other_utils.h 的定义如下:

C 复制代码
#pragma pack(1)
typedef struct {
    int a;
} some_other_struct_t;

#define HELLO(from) printf("hello from %s\n", from)

在这个头文件中,开发者设置了内存对齐属性(但是忘了恢复)。因此,在该头文件之后导入的任何其它头文件都会遵循这个内存对齐属性------而不是遵循在结构体定义时设定的属性。

修复方法

既然知道了问题,修复方法倒是很简单:设置后取消,或只针对单个结构体设置,而不要全局设置。

设置后取消

该方法是利用 #pragma pack 的 push 和 pop 方法(也就是大家熟悉的入栈出栈)完成,在使用完成之后,及时恢复属性原来的值:

diff 复制代码
diff --git a/some_other_utils.h b/some_other_utils.h
index 35fe9d6..fa16baa 100644
--- a/some_other_utils.h
+++ b/some_other_utils.h
@@ -1,6 +1,7 @@
-#pragma pack(1)
+#pragma pack(push, 1)
 typedef struct {
     int a;
 } some_other_struct_t;
+#pragma pack(pop)
 
 #define HELLO(from) printf("hello from %s\n", from)

单结构体设置

就像 hm_t,使用 __attribute__ 指定单个结构体的属性,而不要全局指定:

diff 复制代码
diff --git a/some_other_utils.h b/some_other_utils.h
index 35fe9d6..76d4525 100644
--- a/some_other_utils.h
+++ b/some_other_utils.h
@@ -1,6 +1,5 @@
-#pragma pack(1)
 typedef struct {
     int a;
-} some_other_struct_t;
+} some_other_struct_t __attribute__((aligned(1)));
 
 #define HELLO(from) printf("hello from %s\n", from)

资料

本文代码已上传至:focksor/notebook_demo__c_param_pack_issue

相关推荐
AI逐月17 小时前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
BackCatK Chen18 小时前
第 1 篇:软件视角扫盲|TMC2240 软件核心特性 + 学习路径(附工具清单)
c语言·stm32·单片机·学习·电机驱动·保姆级教程·tmc2240
小白跃升坊18 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey18 小时前
【Linux】线程同步与互斥
linux·笔记
舰长11518 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
zmjjdank1ng18 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.18 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
梵刹古音18 小时前
【C语言】 格式控制符与输入输出函数
c语言·开发语言·嵌入式
VekiSon19 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
无限进步_19 小时前
面试题 02.02. 返回倒数第 k 个节点 - 题解与详细分析
c语言·开发语言·数据结构·git·链表·github·visual studio