C语言:数组的堆栈中内存对齐是如何发生的

C language: how memory alignment happened in the stack for array

所有,我有一个关于 C 中数组内存对齐的有趣问题。我的 OS 是 32 位 Ubuntu,我用 gcc -S -fno-stack-protector 选项编译它。

代码:

char array1[5] = "aaaaa";
char array2[8];
array2[0] = 'b';

汇编代码:

pushl %ebp
move %esp, %ebp.         # esp and ebp are pointing to the same words
subl    , %esp        # move esp to lower 16
movl    33771873, -5(%ebp)       # input "aaaa"
movb    , -1(%ebp).              # input 'a'
movb    , -13(%ebp)              # input 'b'
movl    [=11=], %eax
leave

我有 GDB 检查内存,

%ebpefe8

%espefd8,

&buf1efe3,

&buf2efdb.

在GDB中,我运行x/4bd 0xbfffefd8,它显示

0xbfffefd8:    9  -124   4  98

如果我运行 x/bd 0xbffffed8,它显示

0xbfffefd8:    9

如果我运行 x/bd 0xbffffefdb,它显示

0xbfffefd8:    98

所以记忆是这样的

## high address ##
?                       efe8 <-- ebb
97  97  97  97          efe4 
0  -80  -5  97(a)       efe0
0    0   0   0          efdc
9 -124   4  98(b)       efd8 <-- esp
^            ^
|            |
efd8       efdb

现在我的问题是:

  1. 为什么字符'b'(98)在efdb,而%espefd8?我认为 'b' 也应该在 efd8,因为它是 4 字节字的开始。另外,如果我一直往'b'填到buf2,从efdb开始,只能填5'b',不能填8,怎么会呢?那么'\0'呢?

同样的事情发生在 buf1,它从 efe3 开始,而不是 efe0。这是一种怎样的对齐方式?这对我来说没有意义。

  1. 从汇编代码来看,没有显示16位对齐,我在其他地方看到的,像这样,
andl $-16, %esp     # this aligns esp to 16 boundary

andl 命令什么时候显示,什么时候不显示?它很常见,所以我希望在每个节目中都能看到它。

从上面的汇编代码中,我看不出内存对齐。总是如此吗?我的理解是,汇编代码只是将高级代码(非常可读)解释为不太可读的代码,但仍然转换了确切的消息,因此 char[5] 没有被解释为考虑内存对齐的方式。那么内存对齐应该发生在运行ning时间。我对吗?但是GDB调试显示的和汇编代码完全一样。根本没有对齐。

谢谢。

我看不出有什么问题。 TLDR 答案:char 数组对齐到 1 个字节,编译器是正确的。

再深入一点。在我的 64 位机器上,使用带有 -m32 选项的 GCC 7,我 运行 调试了相同的代码,我得到了相同的结果:

(gdb) x/4bd $esp+12
0xffffcdd4:     97      97      97      97
(gdb) x/4bd $esp+8 
0xffffcdd0:     0       -48     -7      97
(gdb) x/4bd $esp+4
0xffffcdcc:     0       0       0       0
(gdb) x/4bd $esp+0 
0xffffcdc8:     41      85      85      98

地址当然不同,这没关系。现在,让我试着解释一下。 首先,$esp,按预期对齐 4 字节:

(gdb) p $esp
 = (void *) 0xffffcdc8

到目前为止,还不错。现在,因为我们知道 char 数组默认使用 1 作为对齐方式,所以让我们试着弄清楚编译时发生了什么。首先,编译器看到 array1[5] 并将其放入堆栈,但因为它有 5 个字节宽,所以它已将其扩展到第二个双字。因此,第一个双字充满了 'a' 而第二个双字仅使用了一个字节。现在,array2[8] 紧跟在 array1[5] 之后(或之前,取决于您的看法)。它扩展到 3 个双字,以 $esp.

指向的双字结束

所以,我们有:

[esp +  0] <3 bytes of garbage /* no var */>, 'b' /* array2 */,
[esp +  4] 0x0, 0x0, 0x0, 0x0, /* still array2 */
[esp +  8] <3 bytes of garbage /* still array2 */>, 'a' /* array1 */,
[esp + 12] 'a', 'a', 'a', 'a', /* still array1 */.

如果你在 array2 之后添加一个 char[2] 数组,你会看到它使用 $esp 指向的相同双字并且仍然有来自 $esp 的 1 字节垃圾到你的 array3[2].

编译器绝对允许这样做。如果您希望 char 数组以 4 字节对齐(但您需要一个 good 理由!),您必须使用特殊的编译器属性,例如:

__attribute__ ((aligned(4)))