C 内存对齐踩坑记录

发布于:2025-09-04 ⋅ 阅读:(22) ⋅ 点赞:(0)

概要

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

背景

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

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

问题表现

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

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

typedef struct {
    bool a;
    bool b;
    int c __attribute__((aligned(64)));
} hm_t __attribute__((aligned(128)));

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

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 的使用方法也完全相同:

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;
}

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

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 如下:

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"

注意到中间有一段不同:

> # 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 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 的定义如下:

#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 --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 --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