背景
最近在刷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,所以不存在"动态申请"一说,侧面佐证了该内存空间是预先分配好留给用户使用的,接下来我们继续分析。
- 在执行第18行的时候,也就是还没有真正定义m数组时,m数组时多大呢?
(gdb) focus cmd
(gdb) p sizeof(m)
$1 = 16784088
- 执行到第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,超过该值可能不会发生复位,至少目前来看如此,但并不推荐使用超过该区间的内存,因为行为是未定义的
说明
- 转载请注明出处,不要删改文章内容
- 不要转载到V2EX,因为后面我要转载,目前注册时间不足暂时不让发帖