C语言数组的非常量声明

C语言数组的非常量声明

MidCHeck 820 2022-04-26

前言

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处不知道是什么作用, 但从其他部分代码处可以看到运行时动态分配了数组空间。

结论

在栈上动态分配数组方式确实有效,但使用时需严格注意该数组作用域。


# C语言 # 编程 # 数组