GCC 改变变量声明的顺序

逆向工程 数据库 海湾合作委员会
2021-07-10 08:54:21

我有一段非常简单的代码:

// test.c
int main(){
  int a = 0;
  char b[10];
  int c = 0;

  return 0;
}

用 gcc (6.2.1) 编译:

$ gcc -g -o test test.c

并用gdb分析:

$ gdb -q test
(gdb) break 6                (the return statement)
(gdb) run
(gdb) x &a
0x7fffffffecb8: 0x00000000
(gdb) x b
0x7fffffffecc0: 0x00000001
(gdb) x &c
0x7fffffffecbc: 0x00000000

所以你可以很清楚的看到,在内存中,变量是按这个顺序定义的:a,c,b。

有什么原因吗?

1个回答

我最好的猜测:内存对齐。

C 中的整数是 4 个字节,一个字符是 1 个字节。因此,您的声明是这样的:4B & 1x10=10B & 4B。这个顺序意味着 10B 不会在不浪费空间的情况下在 2 次方内存边界上对齐。当数组在 16B/32B/64B 上对齐时,x86 机器上的数据访问速度更快 - 顺便说一句,64B 是缓存行的大小)。

因此,编译器发现将前两个 4B 变量 0x---ECCB8 & 0x---ECBC(相差 4 个字节)对齐更为理想。然后为数组选择最近对齐的内存位置0x---ECC0(0x---ECC0 - 0x---ECBC = 4B);末尾的零表示地址可以被 2 的幂整除。如果您忘记了 7F...并将 ECC0 转换为基数 10,您将得到 60608,它可以被 64、32、16 整除...我们可以安全地假设编译器优化了数组布局以匹配 64B 内存对齐。

您应该阅读Ulrich Drepper关于内存的文档并查看 Agner Fog 的软件优化手册金矿!

附录:

如果您想尝试内存对齐,我建议您查看编译器如何在C 中执行数据布局重组和结构填充填充意味着编译器有时会添加额外的字节以达到 2 的幂边界。例如,假设您有一个包含以下内容的代码

    typedef struct { int x; int y; int z; } point3D;

此声明包含三个 4B 变量 = 12B,而不是 2 的幂。编译器很可能会添加另一个 4B 以使其与最接近的 2 次幂边界对齐:16B。因此,编译后您的声明将如下所示:

    typedef struct {int x; int y; int z; char[4] padding; } point3D;

现在关于您的评论,我建议您尝试以下操作:

       #include <stdio.h>
       #include <emmintrin.h> //Required for mm_malloc
       #define ALIGN 64
       int main(){
       int *a = _mm_malloc(sizeof(int), ALIGN);
       char b[10];
       int *c = _mm_malloc(sizeof(int), ALIGN);

       printf("a: %p\nb: %p\nc: %p, a, b, c);

       return 0; }

malloc有许多变体(以及普通malloc 的技巧),它们可以让您在适合代码的边界上对齐内存。请记住,二进制程序有两种类型的内存:使用malloc类型函数操作和留给编译器处理的堆栈(函数参数、寄存器保存、局部变量等)。 )。您可以控制堆栈使用和数据对齐的唯一方法是使用汇编代码自己完成,或者,如果编译器处理它,则使用编译器指令和参数。

您的代码将三个局部变量分配给main. 因此,它们将在堆栈中分配,并且假设您的代码通过编译器,它将执行所有必要的操作以使用启发式分析(乐观预测内存放置)对齐这些变量。如果您编译上面提供的代码,您会注意到地址并不相似(堆与堆栈)。0x7FF---?对于堆栈变量和 0x000---?对于堆变量。您可以尝试使用这些函数并为自己想出很多东西。有疑问,请参阅文档:英特尔软件优化手册,Agner's,...如果您想清楚地了解事情,下一步是深入研究操作系统内存分配和分页方案以及 x86 架构,以便更好地了解对对齐:缓存行大小、内存分段和 BSI(基础 + 规模 * 索引;

我希望这个附录能让你更清楚。