C99如何支持"动态数组"?

1,792 阅读3分钟

背景

最近在刷OJ算法题,而C语言本身没有string这种能在运行时自动申请内存的类,而为了节省内存,在为未知大小的输入缓存申请内存时,我经常这样做:

int n;
while(scanf("%d", &n) == 1){
    int input[n];
    ...
}

C语言是如何支持这样的定义的呢?它咋个知道用户可能申请多大空间?这就是我验证这个突然间想法的出发点

一个测试DEMO

我猜测是提前预留了一片内存,为了验证这个假设,我写了一下这样的一个DEMO //dyn_arr.c

#include <stdio.h>
int main(void){
    int n;
    scanf("%d", &n);
    int m[n];
    printf("%d\n", (int)sizeof(m));
}

编译:
# gcc -g -Wall -o main dyn_arr.c
执行结果:
>9
36

可以看到,这个数组的大小,确实就是我键入的大小(9x4=36bytes),那它究竟如何实现?接下来我们通过GDB查看它底层的汇编实现,看看有没有所谓的动态内存申请

GDB调试进程

# gdb -q -tui main
(gdb) layout split
(gdb) start
(gdb) n
   ┌──dyn_arr.c───────────────────────────────────────
   │15      int main(void){
   │16              int n;
  >│17              scanf("%d", &n);
   │18              int m[n];
   │19              printf("%d\n", (int)sizeof(m));
   │20      }
   │21
   │22      #ifdef __cplusplus
   │23      }
   │24      #endif
   ┌──────────────────────────────────────────────────
   │0x4005a0 <main+8>       push   %r13
   │0x4005a2 <main+10>      push   %r12
   │0x4005a4 <main+12>      push   %rbx
   │0x4005a5 <main+13>      sub    $0x28,%rsp
   │0x4005a9 <main+17>      mov    %rsp,%rax
   │0x4005ac <main+20>      mov    %rax,%rbx
  >│0x4005af <main+23>      lea    -0x44(%rbp),%rax
   │0x4005b3 <main+27>      mov    %rax,%rsi
   │0x4005b6 <main+30>      mov    $0x40074c,%edi
   │0x4005bb <main+35>      mov    $0x0,%eax

对比源码和汇编代码,并没有发现隐式的内存申请调用malloc,所以不存在"动态申请"一说,侧面佐证了该内存空间是预先分配好留给用户使用的,接下来我们继续分析。

  1. 在执行第18行的时候,也就是还没有真正定义m数组时,m数组时多大呢?
(gdb) focus cmd
(gdb) p sizeof(m)
$1 = 16784088
  1. 执行到第19行后呢?
   ┌──dyn_arr.c────────────────────────────────────
   │15      int main(void){
   │16              int n;
   │17              scanf("%d", &n);
   │18              int m[n];
  >│19              printf("%d\n", (int)sizeof(m));
   │20      }
   │21
   │22      #ifdef __cplusplus
   │23      }
   │24      #endif
   ┌───────────────────────────────────────────────
   │0x400619 <main+129>     add    $0x3,%rax
   │0x40061d <main+133>     shr    $0x2,%rax
   │0x400621 <main+137>     shl    $0x2,%rax
   │0x400625 <main+141>     mov    %rax,-0x40(%rbp)
  >│0x400629 <main+145>     movslq %ecx,%rax
   │0x40062c <main+148>     shl    $0x2,%eax
(gdb) p sizeof(m)
$2 = 36

所以,问题到此,已经明朗,C99是通过预先分配一定的内存大小给这个数组使用,当用户输入需要定义的大小后,便是该数组的实际大小

输入值超过16390KB/4后,进程会崩溃吗?

既然是预先分配的内存空间,按正常逻辑,超过该上限后必然导致进程段错误,是否如此呢?我们验证下:

# ./main
>9
4196023
Segmentation fault
#

确实,当键入的数组元素超过4196022(16784088/4)后,会导致进程复位(实际测试的上限值要更小一些),所以相对C++来说,即便C99能支持这样的数组定义方式,但仍然是存在使用风险的,解决方法就是对输入检测,避免用户输入太大的值而引发异常

16784088究竟是分配到哪的内存?

检查main函数运行时的映射关系:

# pmap `pidof main`
2218:   ./main
0000000000400000      4K r-x--  /home/code/case/dyn_arr/main
0000000000600000      4K rw---  /home/code/case/dyn_arr/main
00007f6e81080000   1736K r-x--  /lib64/libc-2.15.so
00007f6e81232000   2048K -----  /lib64/libc-2.15.so
00007f6e81432000     16K r----  /lib64/libc-2.15.so
00007f6e81436000      8K rw---  /lib64/libc-2.15.so
00007f6e81438000     16K rw---    [ anon ]
00007f6e8143c000    132K r-x--  /lib64/ld-2.15.so
00007f6e8164d000     12K rw---    [ anon ]
00007f6e8165b000      8K rw---    [ anon ]
00007f6e8165d000      4K r----  /lib64/ld-2.15.so
00007f6e8165e000      4K rw---  /lib64/ld-2.15.so
00007f6e8165f000      4K rw---    [ anon ]
00007fff26861000     84K rw---    [ stack ]
00007fff269ff000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
  total             4088K

我们进入GDB一看究竟:

gdb -p `pidof main`
(gdb) b 18
(gdb) c
//另一终端键入数组大小后,触发断点
Breakpoint 1, main () at dyn_arr.c:18
18              int m[n];
(gdb) p &m
$1 = (int (*)[4196022]) 0x7fff26872e78
(gdb) n
19              printf("%d\n", (int)sizeof(m));
(gdb) p &m
$2 = (int (*)[9]) 0x7fff26872d00

可以看到这个可变长度数组的地址为0x7fff26872d00,位于栈空间:00007fff26861000 84K rw--- [ stack ] 这个区域有高达84K的内存空间可用,后跟4K匿名空间,加起来88K的已映射内存,而实际测试结果显示,当键入数组元素数量超过419300(也就是数组大小为1637KB)时仍然没有发生段错误

合法栈空间区间

16390KB从而而来暂时未知,但我们可以查看一个安全的最大栈空间:

[root@localhost dyn_arr]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 14743
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 10240
cpu time               (seconds, -t) unlimited
max user processes              (-u) 14743
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

找到stack size(也可以使用ulimit -s来获取),便可得知安全的栈空间大小为10240KB,超过该值可能不会发生复位,至少目前来看如此,但并不推荐使用超过该区间的内存,因为行为是未定义的

说明

  1. 转载请注明出处,不要删改文章内容
  2. 不要转载到V2EX,因为后面我要转载,目前注册时间不足暂时不让发帖