前言
在C语言中文网中关于C数组类型有三类:
已知常量大小的数组、变长度数组,以及未知大小数组。
变长度数组即本文所讨论的非常量声明,未知大小数组在函数传数组参数时较为常见,此外还有0长大小的柔性数组(C99之前依赖编译器特性)。
背景
在阅读shadowsocks-libev源码时,发现有这么一行:
// initialize listen context
listen_ctx_t listen_ctx_list[server_num];
在以往的知识里,声明数组大小时必须是常量表达式,然而此处发现却与以往认知相左,于是便有了本文的探究。
测试
测试环境为ubuntu20.04,x86_64,gcc 9.4.0,测试代码如下:
#include <stdio.h>
int main(int argc, char **argv)
{
int num;
scanf("%d", &num);
int array[num];
printf("%d\n", sizeof(array)/sizeof(array[0]));
return 0;
}
结果:
$ gcc -o test test.c
$ ./test
10
10
$ ./test
-1
-1
证明此种声明方式确实有效。添加编译标志-std=gnu89,更换编译器为gcc 4.8.4,32位,都可以编译通过,并且正常运行(输入负数打印结果不一致不在本文讨论)。
在ss-libev源码中,此行代码在任何编译器宏下都能到达,意味着此方式声明数组不与特定编译器相关。
其它发现
不能在声明时初始化数组,如下代码:
int array[num] = {0};
编译器报错信息如下:
$ gcc -o test test.c
test.c:6:5: error: variable-sized object may not be initialized
那么这种变长度数组又是如何实现的呢,查看一下它的汇编。
汇编分析
使用gdb查看该段代码汇编(intel语法):
$ gcc -o test test.c -g
0x00000000080011ba <+49>: lea rax,[rbp-0x4c] # rbp-0x4c处为变量num的地址
0x00000000080011be <+53>: mov rsi,rax
0x00000000080011c1 <+56>: lea rdi,[rip+0xe3c] # 0x8002004
0x00000000080011c8 <+63>: mov eax,0x0
0x00000000080011cd <+68>: call 0x8001090 <__isoc99_scanf@plt>
=> 0x00000000080011d2 <+73>: mov ecx,DWORD PTR [rbp-0x4c] # 将num值放入ecx寄存器中
0x00000000080011d5 <+76>: movsxd rax,ecx # 以下算法不知作用,待补充
0x00000000080011d8 <+79>: sub rax,0x1
0x00000000080011dc <+83>: mov QWORD PTR [rbp-0x48],rax
0x00000000080011e0 <+87>: movsxd rax,ecx
0x00000000080011e3 <+90>: mov r14,rax
0x00000000080011e6 <+93>: mov r15d,0x0
0x00000000080011ec <+99>: movsxd rax,ecx
0x00000000080011ef <+102>: mov r12,rax
0x00000000080011f2 <+105>: mov r13d,0x0
0x00000000080011f8 <+111>: movsxd rax,ecx
0x00000000080011fb <+114>: lea rdx,[rax*4+0x0]
0x0000000008001203 <+122>: mov eax,0x10
0x0000000008001208 <+127>: sub rax,0x1
0x000000000800120c <+131>: add rax,rdx
0x000000000800120f <+134>: mov edi,0x10
0x0000000008001214 <+139>: mov edx,0x0
0x0000000008001219 <+144>: div rdi # 以下将分配的栈与4k对齐
0x000000000800121c <+147>: imul rax,rax,0x10
0x0000000008001220 <+151>: mov rdx,rax
0x0000000008001223 <+154>: and rdx,0xfffffffffffff000
0x000000000800122a <+161>: mov rsi,rsp
0x000000000800122d <+164>: sub rsi,rdx
0x0000000008001230 <+167>: mov rdx,rsi
0x0000000008001233 <+170>: cmp rsp,rdx
0x0000000008001236 <+173>: je 0x800124a <main+193>
0x0000000008001238 <+175>: sub rsp,0x1000 # 如果分配大于4k则栈抬高4k
0x000000000800123f <+182>: or QWORD PTR [rsp+0xff8],0x0
0x0000000008001248 <+191>: jmp 0x8001233 <main+170>
0x000000000800124a <+193>: mov rdx,rax # 栈4k块分配完毕
0x000000000800124d <+196>: and edx,0xfff
0x0000000008001253 <+202>: sub rsp,rdx # 栈抬高4k以内的大小,num小于4k时就是num
0x0000000008001256 <+205>: mov rdx,rax
0x0000000008001259 <+208>: and edx,0xfff
0x000000000800125f <+214>: test rdx,rdx # 当num大于4k时,
0x0000000008001262 <+217>: je 0x8001274 <main+235>
0x0000000008001264 <+219>: and eax,0xfff
0x0000000008001269 <+224>: sub rax,0x8
0x000000000800126d <+228>: add rax,rsp
0x0000000008001270 <+231>: or QWORD PTR [rax],0x0 # 检查栈位置能否有读写权限
0x0000000008001274 <+235>: mov rax,rsp
0x0000000008001277 <+238>: add rax,0x3 # array数组4字节对齐
0x000000000800127b <+242>: shr rax,0x2
0x000000000800127f <+246>: shl rax,0x2
0x0000000008001283 <+250>: mov QWORD PTR [rbp-0x40],rax # 将数组的地址放在array变量里
0x0000000008001287 <+254>: movsxd rax,ecx # 计算sizeof大小并准备printf的参数
0x000000000800128a <+257>: shl rax,0x2
0x000000000800128e <+261>: shr rax,0x2
0x0000000008001292 <+265>: mov rsi,rax
0x0000000008001295 <+268>: lea rdi,[rip+0xd6b] # 0x8002007
0x000000000800129c <+275>: mov eax,0x0
0x00000000080012a1 <+280>: call 0x8001080 <printf@plt>
虽然0x80011d5 - 0x800120c处不知道是什么作用, 但从其他部分代码处可以看到运行时动态分配了数组空间。
结论
在栈上动态分配数组方式确实有效,但使用时需严格注意该数组作用域。