項(xiàng)目的代碼中出現(xiàn)的一個(gè)問(wèn)題,問(wèn)題的表現(xiàn)是,在一個(gè)函數(shù)中使用到了變長(zhǎng)數(shù)組,而對(duì)超過(guò)這個(gè)數(shù)組
范圍的一個(gè)賦值,導(dǎo)致了數(shù)組首地址為空.
我把這個(gè)問(wèn)題抽出來(lái)形成了一個(gè)示例函數(shù),在i386下也出現(xiàn)類(lèi)似的問(wèn)題,代碼如下:
#include <stdio.h>
int test(int n)
{
char *arg[n + 4];
printf("before:arg = %p\n", &arg[0]);
arg[16] = NULL;
printf("after:arg = %p\n", &arg[0]);
return 0;
}
int main()
{
test(2);
return 0;
}
這段代碼在i386平臺(tái)下面,執(zhí)行完"arg[16] = NULL"語(yǔ)句之后,再打印arg的首地址,顯示為NULL
使用gdb跟蹤這個(gè)問(wèn)題:
(gdb) b test
Breakpoint 1 at 0x804835b: file test.c, line 4.
(gdb) display /i $pc
(gdb) run
Starting program: /home/lichuang/test/a.out
Breakpoint 1, test (n=2) at test.c:4
4 {
1: x/i $pc 0x804835b <test+7>: mov %esp,%eax
(gdb) si
0x0804835d 4 {
1: x/i $pc 0x804835d <test+9>: mov %eax,%ebx
(gdb)
5 char *arg[n + 4];
1: x/i $pc 0x804835f <test+11>: mov 0x8(%ebp),%eax
(gdb)
0x08048362 5 char *arg[n + 4];
1: x/i $pc 0x8048362 <test+14>: add $0x4,%eax
(gdb)
0x08048365 5 char *arg[n + 4];
1: x/i $pc 0x8048365 <test+17>: shl $0x2,%eax
(gdb)
0x08048368 5 char *arg[n + 4];
1: x/i $pc 0x8048368 <test+20>: add $0xf,%eax
(gdb)
0x0804836b 5 char *arg[n + 4];
1: x/i $pc 0x804836b <test+23>: add $0xf,%eax
(gdb)
0x0804836e 5 char *arg[n + 4];
1: x/i $pc 0x804836e <test+26>: shr $0x4,%eax
(gdb)
0x08048371 5 char *arg[n + 4];
1: x/i $pc 0x8048371 <test+29>: shl $0x4,%eax
(gdb)
0x08048374 5 char *arg[n + 4];
1: x/i $pc 0x8048374 <test+32>: sub %eax,%esp
(gdb)
0x08048376 5 char *arg[n + 4];
1: x/i $pc 0x8048376 <test+34>: lea 0x8(%esp),%eax
(gdb)
0x0804837a 5 char *arg[n + 4];
1: x/i $pc 0x804837a <test+38>: mov %eax,0xffffffe8(%ebp)
(gdb)
0x0804837d 5 char *arg[n + 4];
1: x/i $pc 0x804837d <test+41>: mov 0xffffffe8(%ebp),%eax
(gdb)
0x08048380 5 char *arg[n + 4];
1: x/i $pc 0x8048380 <test+44>: add $0xf,%eax
(gdb)
0x08048383 5 char *arg[n + 4];
1: x/i $pc 0x8048383 <test+47>: shr $0x4,%eax
(gdb)
0x08048386 5 char *arg[n + 4];
1: x/i $pc 0x8048386 <test+50>: shl $0x4,%eax
(gdb)
0x08048389 5 char *arg[n + 4];
1: x/i $pc 0x8048389 <test+53>: mov %eax,0xffffffe8(%ebp)
(gdb)
0x0804838c 5 char *arg[n + 4];
1: x/i $pc 0x804838c <test+56>: mov 0xffffffe8(%ebp),%eax
(gdb)
0x0804838f 5 char *arg[n + 4];
1: x/i $pc 0x804838f <test+59>: mov %eax,0xfffffff8(%ebp)
(gdb)
7 printf("before:arg = %p\n", &arg[0]);
1: x/i $pc 0x8048392 <test+62>: mov 0xfffffff8(%ebp),%eax
上面是使用gdb跟蹤匯編代碼顯示的結(jié)果,可以看到,在定義變長(zhǎng)數(shù)組arg[n + 4]的時(shí)候,執(zhí)行了很多語(yǔ)句,秘密都在這些匯編代碼里面了,把這個(gè)程序用objdump -d命令反匯編出來(lái),抽出上面的那部分匯編代碼查看:
804835b: 89 e0 mov %esp,%eax
804835d: 89 c3 mov %eax,%ebx
804835f: 8b 45 08 mov 0x8(%ebp),%eax
8048362: 83 c0 04 add $0x4,%eax
8048365: c1 e0 02 shl $0x2,%eax
8048368: 83 c0 0f add $0xf,%eax
804836b: 83 c0 0f add $0xf,%eax
804836e: c1 e8 04 shr $0x4,%eax
8048371: c1 e0 04 shl $0x4,%eax
8048374: 29 c4 sub %eax,%esp
8048376: 8d 44 24 08 lea 0x8(%esp),%eax
804837a: 89 45 e8 mov %eax,0xffffffe8(%ebp)
804837d: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
8048380: 83 c0 0f add $0xf,%eax
8048383: c1 e8 04 shr $0x4,%eax
8048386: c1 e0 04 shl $0x4,%eax
8048389: 89 45 e8 mov %eax,0xffffffe8(%ebp)
804838c: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
804838f: 89 45 f8 mov %eax,0xfffffff8(%ebp)
8048392: 8b 45 f8 mov 0xfffffff8(%ebp),%eax
逐句進(jìn)行分析如下:
804835b: 89 e0 mov %esp,%eax
804835d: 89 c3 mov %eax,%ebx
將esp寄存器地址通過(guò)eax保存到ebx寄存器中
804835f: 8b 45 08 mov 0x8(%ebp),%eax
8048362: 83 c0 04 add $0x4,%eax
首先獲得傳入test函數(shù)的參數(shù)n的值(在內(nèi)存地址為ebp+8的位置),再將它的值加上4,也就得到了數(shù)組arg[n+4]的元素?cái)?shù)量
8048365: c1 e0 02 shl $0x2,%eax
8048368: 83 c0 0f add $0xf,%eax
804836b: 83 c0 0f add $0xf,%eax
804836e: c1 e8 04 shr $0x4,%eax
8048371: c1 e0 04 shl $0x4,%eax
首先將前面得到的元素?cái)?shù)量左移兩位(shl 0x2),也就是乘以4,4是sizeof(char*)的大小,于是得到了char*
arg[n+4]所容納元素的空間大小.之后兩次加上0xf,然后又右移4位左移4位的原因是,編譯器要將這個(gè)大小按照16來(lái)對(duì)齊,而又要留夠足夠的空
間,所以前面兩次加上0xf.在上面幾個(gè)操作完成之后,eax里面的值就是可以容納char* arg[n+4]的按照16對(duì)齊的數(shù)據(jù)
8048374: 29 c4 sub %eax,%esp
根據(jù)前面得到的eax值調(diào)整esp指針,也就是在test函數(shù)的棧幀地址的低位置留出了足夠容納arg數(shù)組的空間.
注意到,esp值已經(jīng)在最開(kāi)始保存到ebx寄存器中了,所以,在test函數(shù)的結(jié)束位置,還要使用ebx寄存器恢復(fù)esp寄存器.
8048376: 8d 44 24 08 lea 0x8(%esp),%eax
804837a: 89 45 e8 mov %eax,0xffffffe8(%ebp)
804837d: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
8048380: 83 c0 0f add $0xf,%eax
8048383: c1 e8 04 shr $0x4,%eax
8048386: c1 e0 04 shl $0x4,%eax
8048389: 89 45 e8 mov %eax,0xffffffe8(%ebp)
804838c: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
804838f: 89 45 f8 mov %eax,0xfffffff8(%ebp)
第一句將地址esp+8賦值到eax中(注意lea指令和mov的區(qū)別,前者是取指針操作,后者是取指針?biāo)赶虻膬?nèi)存數(shù)據(jù)操作),然后將eax賦值到
ebp+0xffffffe8內(nèi)存處,而后面一句又將這個(gè)值賦值回到eax中,我不明白這句話(huà)有什么作用,看上去沒(méi)有什么影響.最后又執(zhí)行前面看到的按照
16對(duì)齊的操作,對(duì)齊后的結(jié)果仍然保存在eax中.這個(gè)值就是最后arg的首地址所在,最后三句將這個(gè)值分別存放到ebp+0xffffffe8和
ebo+0xffffff8處.也就是說(shuō),arg的真實(shí)地址,其實(shí)在內(nèi)存中有兩處進(jìn)行了保存.在gdb中查看,確實(shí)如此:
(gdb) x/20 &arg
0xbfde96e0: 0x00000000 0x00000000 0x00000000 0x00000000
0xbfde96f0: 0x00000000 0x00000000 0x00000000 0x080482e0
0xbfde9700: 0x00000000 0x080495d8 0xbfde9718 0x08048265
0xbfde9710: 0xbfde96e0 0x00632214 0xbfde9748 0x08048429
0xbfde9720: 0xbfde96e0 0x00633ff4 0xbfde9738 0x080483eb
可以看到,查看&arg的內(nèi)存地址后發(fā)現(xiàn),它的地址是0xbfde96e0,而在地址為0xbfde9710和0xbfde9720,都存放著地
址&arg的地址0xbde96e0,它們分別位于&arg[13]和&arg[16]處,而這兩處的賦值,就是前面:
8048389: 89 45 e8 mov %eax,0xffffffe8(%ebp)
804838c: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
804838f: 89 45 f8 mov %eax,0xfffffff8(%ebp)
的結(jié)果.我不清楚為什么會(huì)在兩個(gè)地方保存這個(gè)地址.
所以,在test函數(shù)的代碼中,將arg[16]賦值為NULL之后,再次打印arg的值就是NULL了.
現(xiàn)在,可以得出以下的結(jié)論:
變長(zhǎng)數(shù)組在實(shí)現(xiàn)時(shí)實(shí)際上退化為了指針,原先定長(zhǎng)的數(shù)組,它的地址同時(shí)也就是容納數(shù)組元素位置的首地址,而變長(zhǎng)數(shù)組,退化為了指針,該指針指向的位置才是真
正容納數(shù)組元素的首地址.在對(duì)匯編代碼的分析中,可以看到,在定義變長(zhǎng)數(shù)組arg的時(shí)候,首先通過(guò)函數(shù)參數(shù),以及數(shù)組元素的尺寸
(sizeof(char*))獲得這個(gè)數(shù)組的總大小(需要對(duì)齊),然后調(diào)整esp指針留出空間給數(shù)組.這就是因?yàn)樽冮L(zhǎng)數(shù)組的大小只有在運(yùn)行時(shí)才確定,所
以需要在運(yùn)行時(shí)動(dòng)態(tài)計(jì)算并且在函數(shù)棧幀的最低位置留出空間.
大概的示意圖如下:

BTW:在gdb中,打印&arg可以看到:
(gdb) print &arg
$1 = (char *(*)[0]) 0xbfde96e0
也就是,arg是一個(gè)指針,指向的是一個(gè)元素類(lèi)型為char*的數(shù)組,而這個(gè)數(shù)組的元素是0.這是C中常用的技巧了,定義一個(gè)0元素的數(shù)組,實(shí)際上里面存放的元素?cái)?shù)量是不確定的.這也證明了我們前面提到的:變長(zhǎng)數(shù)組被編譯器退化為指針來(lái)處理.
最后,注意前面的描述都在i386平臺(tái)下面,根據(jù)編譯器,體系結(jié)構(gòu)的不同,可能稍有區(qū)別,即使在同一個(gè)平臺(tái)下面,加不同的優(yōu)化參數(shù)(如-O2等),具體的實(shí)現(xiàn)可能有差異,但是大體上還是上面提到的原理.
我想,從這個(gè)例子里,也可以進(jìn)一步加深對(duì)指針和數(shù)組區(qū)別的理解.