簡單羅列一下 C 的指針用法,便于復(fù)習(xí)。
指針常量意指 "類型為指針的常量",初始化后不能被修改,固定指向某個(gè)內(nèi)存地址。我們無法修改指針自身的值,但可以修改指針?biāo)改繕?biāo)的內(nèi)容。
int x[] = { 1, 2, 3, 4 };
int* const p = x;
for (int i = 0; i < 4; i++)
{
int v = *(p + i);
*(p + i) = ++v;
printf("%d\n", v);
//p++; // Compile Error!
}
上例中的指針 p 始終指向數(shù)組 x 的第一個(gè)元素,和數(shù)組名 x 作用相同。由于指針本身是常量,自然無法執(zhí)行 p++、++p 之類的操作,否則會導(dǎo)致編譯錯(cuò)誤。
常量指針是說 "指向常量數(shù)據(jù)的指針",指針目標(biāo)被當(dāng)做常量處理 (盡管原目標(biāo)不一定是常量),不能用通過指針做賦值處理。指針自身并非常量,可以指向其他位置,但依然不能做賦值操作。
int x = 1, y = 2;
int const* p = &x;
//*p = 100; ! ! // Compile Error!
p = &y;
printf("%d\n", *p);
//*p = 100; ! ! // Compile Error!
建議常量指針將 const 寫在前面更易識別。
const int* p = &x;
看幾種特殊情況:(1) 下面的代碼據(jù)說在 VC 下無法編譯,但 GCC 是可以的。
const int x = 1;
int* p = &x;
printf("%d\n", *p);
*p = 1234;
printf("%d\n", *p);
(2) const int* p 指向 const int 自然沒有問題,但肯定也不能通過指針做出修改。
const int x = 1;
const int* p = &x;
printf("%d\n", *p);
*p = 1234; ! ! ! // Compile Error!
(3) 聲明指向常量的指針常量,這很罕見,但也好理解。
int x = 10;
const int* const p = &i;
p++; ! ! ! ! // Compile Error!
*p = 20; ! ! ! // Compile Error!
區(qū)別指針常量和常量指針方法很簡單:看 const 修飾的是誰,也就是*
在 const 的左邊還是右邊。
*
const p: 指向常量的指針常量。右 const 修飾 p 常量,左 const 表明 *p 為常量。指針本身也是內(nèi)存區(qū)的一個(gè)數(shù)據(jù)變量,自然也可以用其他的指針來指向它。
int x = 10;
int* p = &x;
int** p2 = &p;
printf("p = %p, *p = %d\n", p, *p);
printf("p2 = %p, *p2 = %x\n", p2, *p2);
printf("x = %d, %d\n",*p, **p2);
輸出:
p = 0xbfba3e5c, *p = 10
p2 = 0xbfba3e58, *p2 = bfba3e5c
x = 10, 10
我們可以發(fā)現(xiàn) p2 存儲的是指針 p 的地址。因此才有了指針的指針一說。
默認(rèn)情況下,數(shù)組名為指向該數(shù)組第一個(gè)元素的指針常量。
int x[] = { 1, 2, 3, 4 };
int* p = x;
for (int i = 0; i < 4; i++)
{
printf("%d, %d, %d\n", x[i], *(x + i), , *p++);
}
盡管我們可以用 *(x + 1) 訪問數(shù)組元素,但不能執(zhí)行 x++ / ++x 操作。但 "數(shù)組的指針" 和數(shù)組名并不是一個(gè)類型,數(shù)組指針將整個(gè)數(shù)組當(dāng)做一個(gè)對象,而不是其中的成員(元素)。
int x[] = { 1, 2, 3, 4 };
int* p = x;
int (*p2)[] = &x; ! ! // 數(shù)組指針
for(int i = 0; i < 4; i++)
{
printf("%d, %d\n", *p++, (*p2)[i]);
}
更多詳情參考《數(shù)組指針》。
元素類型為指針的數(shù)組稱之為指針數(shù)組。
int x[] = { 1, 2, 3, 4 };
int* ps[] = { x, x + 1, x + 2, x + 3 };
for(int i = 0; i < 4; i++)
{
printf("%d\n", *(ps[i]));
}
x 默認(rèn)就是指向第?一個(gè)元素的指針,那么 x + n 自然獲取后續(xù)元素的指針。
指針數(shù)組通常?用來處理交錯(cuò)數(shù)組 (Jagged Array,又稱數(shù)組的數(shù)組,不是二維數(shù)組),最常見的就是字符串?dāng)?shù)組了。
void test(const char** x, int len)
{
for (int i = 0; i < len; i++)
{
printf("test: %d = %s\n", i, *(x + i));
}
}
int main(int argc, char* argv[])
{
char* a = "aaa";
char* b = "bbb";
char* ss[] = { a, b };
for (int i = 0; i < 2; i++)
{
printf("%d = %s\n", i, ss[i]);
}
test(ss, 2);
return EXIT_SUCCESS;
}
更多詳情參考《指針數(shù)組》。
默認(rèn)情況下,函數(shù)名就是指向該函數(shù)的指針常量。
void inc(int* x)
{
*x += 1;
}
int main(void)
{
void (*f)(int*) = inc;
int i = 100;
f(&i);
printf("%d\n", i);
return 0;
}
如果嫌函數(shù)指針的聲明不好看,可以像 C# 委托那樣定義一個(gè)函數(shù)指針類型。
typedef void (*inc_t)(int*);
int main(void)
{
inc_t f = inc;
... ...
}
很顯然,有了 typedef,下面的代碼更易閱讀和理解。
inc_t getFunc()
{
return inc;
}
int main(void)
{
inc_t inc = getFunc();
... ...
}
注意:
void test()
{
printf("test");
}
typedef void(func_t)();
typedef void(*func_ptr_t)();
int main(int argc, char* argv[])
{
func_t* f = test;
func_ptr_t p = test;
f();
p();
return EXIT_SUCCESS;
}
注意下面代碼中指針的區(qū)別。
int x[] = {1,2,3,4,5,6};
int *p1 = x; ! // 指向整數(shù)的指針
int (*p2)[] = &x; ! // 指向數(shù)組的指針
p1 的類型是 int,也就是說它指向一個(gè)整數(shù)類型。數(shù)組名默認(rèn)指向數(shù)組中的第一個(gè)元素,因此 x 默認(rèn)也是 int 類型。
p2 的含義是指向一個(gè) "數(shù)組類型" 的指針,注意是 "數(shù)組類型" 而不是 "數(shù)組元素類型",這有本質(zhì)上的區(qū)別。
數(shù)組指針把數(shù)組當(dāng)做一個(gè)整體,因?yàn)閺念愋徒嵌葋碚f,數(shù)組類型和數(shù)組元素類型是兩個(gè)概念。因此"p2 = &x" 當(dāng)中 x 代表的是數(shù)組本身而不是數(shù)組的第一個(gè)元素地址,&x 取的是數(shù)組指針,而不是"第一個(gè)元素指針的指針"。
接下來,我們看看如何用數(shù)組指針操作一維數(shù)組。
void array1()
{
int x[] = {1,2,3,4,5,6};
int (*p)[] = &x; // 指針 p 指向數(shù)組
for(int i = 0; i < 6; i++)
{
printf("%d\n", (*p)[i]); // *p 返回該數(shù)組, (*p)[i] 相當(dāng)于 x[i]
}
}
有了上面的說明,這個(gè)例子就很好理解了。
"p = &x" 使得指針 p 存儲了該數(shù)組的指針,p 自然就是獲取該數(shù)組。那么 (p)[i] 也就等于 x[i]。注意: p 的目標(biāo)類型是數(shù)組,因此 p++ 指向的不是數(shù)組下一個(gè)元素,而是 "整個(gè)數(shù)組之后" 位置 (EA + SizeOf(x)),這已經(jīng)超出數(shù)組范圍了。
數(shù)組指針對二維數(shù)組的操作。
void array2()
{
int x[][4] = {{1, 2, 3, 4}, {11, 22, 33, 44}};
int (*p)[4] = x; !! ! ! ! // 相當(dāng)于 p = &x[0]
for(int i = 0; i < 2; i++)
{
for (int c = 0; c < 4; c++)
{
printf("[%d, %d] = %d\n", i, c, (*p)[c]);
}
p++;
}
}
x 是一個(gè)二維數(shù)組,x 默認(rèn)指向該數(shù)組的第一個(gè)元素,也就是 {1,2,3,4}。不過要注意,這第一個(gè)元素不是 int,而是一個(gè) int[],x 實(shí)際上是 int()[] 指針。因此 "p = x" 而不是 "p = &x",否則 p 就指向 int ()[][] 了。
既然 p 指向第一個(gè)元素,那么 p 自然也就是第一行數(shù)組了,也就是 {1,2,3,4},(p)[2] 的含義就是第一行的第三個(gè)元素。p++ 的結(jié)果自然也就是指向下一行。我們還可以直接用 *(p + 1) 來訪問x[1]。
void array2()
{
int x[][4] = {{1, 2, 3, 4}, {11, 22, 33, 44}};
int (*p)[4] = x;
printf("[1, 3] = %d\n", (*(p + 1))[3]);
}
我們繼續(xù)看看 int (*)[][] 的例子。
void array3()
{
int x[][4] = {{1, 2, 3, 4}, {11, 22, 33, 44}};
int (*p)[][4] = &x;
for(int i = 0; i < 2; i++)
{
for (int c = 0; c < 4; c++)
{
printf("[%d, %d] = %d\n", i, c, (*p)[i][c]);
}
}
}
這回 "p = &x",也就是說把整個(gè)二維數(shù)組當(dāng)成一個(gè)整體,因此 *p 返回的是整個(gè)二維數(shù)組,因此 p++ 也就用不得了。
附: 在附有初始化的數(shù)組聲明語句中,只有第一維度可以省略。
將數(shù)組指針當(dāng)做函數(shù)參數(shù)傳遞。
void test1(p,len)
int(*p)[];
int len;
{
for(int i = 0; i < len; i++)
{
printf("%d\n", (*p)[i]);
}
}
void test2(void* p, int len)
{
int(*pa)[] = p;
for(int i = 0; i < len; i++)
{
printf("%d\n", (*pa)[i]);
}
}
int main (int args, char* argv[])
{
int x[] = {1,2,3};
test1(&x, 3);
test2(&x, 3);
return 0;
}
由于數(shù)組指針類型中有括號,因此 test1 的參數(shù)定義看著有些古怪,不過習(xí)慣就好了。
指針數(shù)組是指元素為指針類型的數(shù)組,通常用來處理 "交錯(cuò)數(shù)組",又稱之為數(shù)組的數(shù)組。和二維數(shù)組不同,指針數(shù)組的元素只是一個(gè)指針,因此在初始化的時(shí)候,每個(gè)元素只占用4字節(jié)內(nèi)存空間,比二維數(shù)組節(jié)省。同時(shí),每個(gè)元素?cái)?shù)組的長度可以不同,這也是交錯(cuò)數(shù)組的說法。(在C# 中,二維數(shù)組用 [,] 表示,交錯(cuò)數(shù)組用 [][])
int main(int argc, char* argv[])
{
int x[] = {1,2,3};
int y[] = {4,5};
int z[] = {6,7,8,9};
int* ints[] = { NULL, NULL, NULL };
ints[0] = x;
ints[1] = y;
ints[2] = z;
printf("%d\n", ints[2][2]);
for(int i = 0; i < 4; i++)
{
printf("[2,%d] = %d\n", i, ints[2][i]);
}
return 0;
}
輸出:
8
[2,0] = 6
[2,1] = 7
[2,2] = 8
[2,3] = 9
我們查看一下指針數(shù)組 ints 的內(nèi)存數(shù)據(jù)。
(gdb) x/3w ints
0xbf880fd0: 0xbf880fdc 0xbf880fe8 0xbf880fc0
(gdb) x/3w x
0xbf880fdc: 0x00000001 0x00000002 0x00000003
(gdb) x/2w y
0xbf880fe8: 0x00000004 0x00000005
(gdb) x/4w z
0xbf880fc0: 0x00000006 0x00000007 0x00000008 0x00000009
可以看出,指針數(shù)組存儲的都是目標(biāo)元素的指針。
那么默認(rèn)情況下 ints 是哪種類型的指針呢?按規(guī)則來說,數(shù)組名默認(rèn)是指向第一個(gè)元素的指針,那么第一個(gè)元素是什么呢?數(shù)組?當(dāng)然不是,而是一個(gè) int 的指針而已。注意 "ints[0] = x;" 這條語句,實(shí)際上 x 返回的是 &x[0] 的指針 (int),而非 &a 這樣的數(shù)組指針(int ()[])。繼續(xù),ints 取出第一個(gè)元素內(nèi)容 (0xbf880fdc),這個(gè)內(nèi)容又是一個(gè)指針,因此 ints 隱式成為一個(gè)指針的指針(int**),就交錯(cuò)數(shù)組而言,它默認(rèn)指向 ints[0][0]。
int main(int argc, char* argv[])
{
int x[] = {1,2,3};
int y[] = {4,5};
int z[] = {6,7,8,9};
int* ints[] = { NULL, NULL, NULL };
ints[0] = x;
ints[1] = y;
ints[2] = z;
printf("%d\n", **ints);
printf("%d\n", *(*ints + 1));
printf("%d\n", **(ints + 1));
return 0;
}
輸出:
1
2
4
第一個(gè) printf 語句驗(yàn)證了我們上面的說法。我們繼續(xù)分析后面兩個(gè)看上去有些復(fù)雜的 printf 語句。
(1) (ints + 1)首先 ints 取出了第一個(gè)元素,也就是 ints[0][0] 的指針。那么 "ints + 1" 實(shí)際上就是向后移動(dòng)一次指針,因此指向 ints[0][1] 的指針。"(ints + 1)" 的結(jié)果也就是取出 ints[0][1] 的值了。(2) *(ints + 1)ints 指向第一個(gè)元素 (0xbf880fdc),"ints + 1" 指向第二個(gè)元素(0xbf880fe8)。"(ints + 1)" 取出 ints[1] 的內(nèi)容,這個(gè)內(nèi)容是另外一只指針,因此 "**(ints + 1)" 就是取出 ints[1][0] 的內(nèi)容。
下面這種寫法,看上去更容易理解一些。
int main(int argc, char* argv[])
{
int x[] = {1,2,3};
int y[] = {4,5};
int z[] = {6,7,8,9};
int* ints[] = { NULL, NULL, NULL };
ints[0] = x;
ints[1] = y;
ints[2] = z;
int** p = ints;
// -----------------------------------------------
// *p 取出 ints[0] 存儲的指針(&ints[0][0])
// **p 取出 ints[0][0] 值
printf("%d\n", **p);
// -----------------------------------------------
// p 指向 ints[1]
p++;
// *p 取出 ints[1] 存儲的指針(&ints[1][0])
// **p 取出 ints[1][0] 的值(= 4)
printf("%d\n", **p);
// -----------------------------------------------
// p 指向 ints[2]
p++;
// *p 取出 ints[2] 存儲的指針(&ints[2][0])
// *p + 1 返回所取出指針的后一個(gè)位置
// *(*p + 1) 取出 ints[2][0 + 1] 的值(= 7)
printf("%d\n", *(*p + 1));
return 0;
}
指針數(shù)組經(jīng)常出現(xiàn)在操作字符串?dāng)?shù)組的場合。
int main (int args, char* argv[])
{
char* strings[] = { "Ubuntu", "C", "C#", "NASM" };
for (int i = 0; i < 4; i++)
{
printf("%s\n", strings[i]);
}
printf("------------------\n");
printf("[2,1] = '%c'\n", strings[2][1]);
strings[2] = "CSharp";
printf("%s\n", strings[2]);
printf("------------------\n");
char** p = strings;
printf("%s\n", *(p + 2));
return 0;
}
輸出:
Ubuntu
C
C#
NASM
------------------
[2,1] = '#'
CSharp
------------------
CSharp
main 參數(shù)的兩種寫法。
int main(int argc, char* argv[])
{
for (int i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
int main(int argc, char** argv)
{
for (int i = 0; i < argc; i++)
{
printf("%s\n", *(argv + i));
}
return 0;
}
當(dāng)然,指針數(shù)組不僅僅用來處理數(shù)組。
int main (int args, char* argv[])
{
int* ints[] = { NULL, NULL, NULL, NULL };
int a = 1;
int b = 2;
ints[2] = &a;
ints[3] = &b;
for(int i = 0; i < 4; i++)
{
int* p = ints[i];
printf("%d = %d\n", i, p == NULL ? 0 : *p);
}
return 0;
}
先準(zhǔn)備一個(gè)簡單的例子。
源代碼
#include <stdio.h>
int test(int x, char* s)
{
s = "Ubuntu!";
return ++x;
}
int main(int args, char* argv[])
{
char* s = "Hello, World!";
int x = 0x1234;
int c = test(x, s);
printf(s);
return 0;
}
編譯 (注意沒有使用優(yōu)化參數(shù)):
$ gcc -Wall -g -o hello hello.c
調(diào)試之初,我們先反編譯代碼,并做簡單標(biāo)注。
$ gdb hello
(gdb) set disassembly-flavor intel ; 設(shè)置反匯編指令格式
(gdb) disass main ; 反匯編 main
Dump of assembler code for function main:
0x080483d7 <main+0>: lea ecx,[esp+0x4]
0x080483db <main+4>: and esp,0xfffffff0
0x080483de <main+7>: push DWORD PTR [ecx-0x4]
0x080483e1 <main+10>: push ebp ; main 堆棧幀開始
0x080483e2 <main+11>: mov ebp,esp ; 修正 ebp 基址
0x080483e4 <main+13>: push ecx ; 保護(hù)寄存器現(xiàn)場
0x080483e5 <main+14>: sub esp,0x24 ; 預(yù)留堆棧幀空間
0x080483e8 <main+17>: mov DWORD PTR [ebp-0x8],0x80484f8 ; 設(shè)置變量 s,為字符串地址。
0x080483ef <main+24>: mov DWORD PTR [ebp-0xc],0x1234 ; 變量 x,內(nèi)容為內(nèi)聯(lián)整數(shù)值。
0x080483f6 <main+31>: mov eax,DWORD PTR [ebp-0x8] ; 復(fù)制變量 s
0x080483f9 <main+34>: mov DWORD PTR [esp+0x4],eax ; 將復(fù)制結(jié)果寫入新堆棧位置
0x080483fd <main+38>: mov eax,DWORD PTR [ebp-0xc] ; 復(fù)制變量 x
0x08048400 <main+41>: mov DWORD PTR [esp],eax ; 將復(fù)制結(jié)果寫入新堆棧位置
0x08048403 <main+44>: call 0x80483c4 <test> ; 調(diào)用 test
0x08048408 <main+49>: mov DWORD PTR [ebp-0x10],eax ; 保存 test 返回值
0x0804840b <main+52>: mov eax,DWORD PTR [ebp-0x8] ; 復(fù)制變量 s 內(nèi)容
0x0804840e <main+55>: mov DWORD PTR [esp],eax ; 保存復(fù)制結(jié)果到新位置
0x08048411 <main+58>: call 0x80482f8 <printf@plt> ; 調(diào)用 printf
0x08048416 <main+63>: mov eax,0x0 ; 丟棄 printf 返回值
0x0804841b <main+68>: add esp,0x24 ; 恢復(fù) esp 到堆棧空間預(yù)留前位置
0x0804841e <main+71>: pop ecx ; 恢復(fù) ecx 保護(hù)現(xiàn)場
0x0804841f <main+72>: pop ebp ; 修正前一個(gè)堆棧幀基址
0x08048420 <main+73>: lea esp,[ecx-0x4] ; 修正 esp 指針
0x08048423 <main+76>: ret
End of assembler dump.
(gdb) disass test ! ! ! ! ! ! ; 反匯編 test
Dump of assembler code for function test:
0x080483c4 <test+0>: push ebp ; 保存前一個(gè)堆棧幀的基址
0x080483c5 <test+1>: mov ebp,esp ; 修正 ebp 基址
0x080483c7 <test+3>: mov DWORD PTR [ebp+0xc],0x80484f0 ; 修改參數(shù) s, 是前一堆棧幀地址
0x080483ce <test+10>: add DWORD PTR [ebp+0x8],0x1 ; 累加參數(shù) x
0x080483d2 <test+14>: mov eax,DWORD PTR [ebp+0x8] ; 將返回值存入 eax
0x080483d5 <test+17>: pop ebp ; 恢復(fù) ebp
0x080483d6 <test+18>: ret
End of assembler dump.
我們一步步分析,并用示意圖說明堆棧狀態(tài)。
(1) 在 0x080483f6 處設(shè)置斷點(diǎn),這時(shí)候 main 完成了基本的初始化和內(nèi)部變量賦值。
(gdb) b *0x080483f6
Breakpoint 1 at 0x80483f6: file hello.c, line 14.
(gdb) r
Starting program: /home/yuhen/Projects/Learn.C/hello
Breakpoint 1, main () at hello.c:14
14 int c = test(x, s);
我們先記下 ebp 和 esp 的地址。
(gdb) p $ebp
$8 = (void *) 0xbfcb3c78
(gdb) p $esp
$9 = (void *) 0xbfcb3c50! # $ebp - $esp = 0x28,不是 0x24?在預(yù)留空間前還 "push ecx" 了。
(gdb) p x ! ! ! # 整數(shù)值直接保存在堆棧
$10 = 4660
(gdb) p &x ! ! ! # 變量 x 地址 = ebp (0xbfcb3c78) - 0xc = 0xbfcb3c6c
$11 = (int *) 0xbfcb3c6c
(gdb) p s ! ! ! # 變量 s 在堆棧保存了字符串在 .rodata 段的地址
$12 = 0x80484f8 "Hello, World!"
(gdb) p &s ! ! ! # 變量 s 地址 = ebp (0xbfcb3c78) - 0x8 = 0xbfcb3c70
$13 = (char **) 0xbfcb3c70
這時(shí)候的堆棧示意圖如下:
(2) 接下來,我們將斷點(diǎn)設(shè)在 call test 之前,看看調(diào)用前堆棧的準(zhǔn)備情況。
(gdb) b *0x08048403
Breakpoint 2 at 0x8048403: file hello.c, line 14
(gdb) c
Continuing.
Breakpoint 2, 0x08048403 in main () at hello.c:14
14 int c = test(x, s);
0x08048403 之前的 4 條指令通過 eax 做中轉(zhuǎn),分別在 [esp+0x4] 和 [esp] 處復(fù)制了變量 s、x的內(nèi)容。
(gdb) x/12w $esp
0xbfcb3c50: 0x00001234 0x080484f8 0xbfcb3c68 0x080482c4
0xbfcb3c60: 0xb8081ff4 0x08049ff4 0xbfcb3c88 0x00001234
0xbfcb3c70: 0x080484f8 0xbfcb3c90 0xbfcb3ce8 0xb7f39775
第 1 行: 復(fù)制的變量 x,復(fù)制的變量 s,未使用,未使用第 2 行: 未使用,未使用,未使用,變量 x第 3 行: 變量 s,ecx 保護(hù)值,ebp 保護(hù)值,eip 保護(hù)值。
可以和 frame 信息對照著看。
(gdb) info frame
Stack level 0, frame at 0xbfcb3c80:
eip = 0x8048403 in main (hello.c:14); saved eip 0xb7f39775
source language c.
Arglist at 0xbfcb3c78, args:
Locals at 0xbfcb3c78, Previous frame's sp at 0xbfcb3c74
Saved registers:
ebp at 0xbfcb3c78, eip at 0xbfcb3c7c
說明: 嚴(yán)格來說堆棧幀(frame)從函數(shù)被調(diào)用的 call 指令將 eip 入棧開始,而不是我們通常所指修正后的 ebp 位置。以 ebp 為基準(zhǔn)純粹是為了閱讀代碼方便,本文也以此做示意圖。也就是說在 calltest 之前,內(nèi)存當(dāng)中已經(jīng)有了兩份 s 和 x 。從中我們也看到了 C 函數(shù)參數(shù)是按照從右到左的方式入棧。
附:這種由調(diào)用方負(fù)責(zé)參數(shù)入棧和清理的方式是 C 默認(rèn)的調(diào)用約定 cdecl,調(diào)用者除了參數(shù)入棧,還負(fù)責(zé)堆棧清理。相比 stdcall 的好處就是:cdecl 允許方法參數(shù)數(shù)量不固定。
(3) 在 test 中設(shè)置斷點(diǎn),我們看看 test 中的代碼對堆棧的影響。
(gdb) b test
Breakpoint 3 at 0x80483c7: file hello.c, line 5.
(gdb) c
Continuing.
Breakpoint 3, test (x=4660, s=0x80484f8 "Hello, World!") at hello.c:5
5 s = "Ubuntu!";
main 中的 call 指令會先將 eip 的值入棧,以便函數(shù)完成時(shí)可以恢復(fù)調(diào)用位置。然后才是跳轉(zhuǎn)到 test 函數(shù)地址入口。因此我們在 test 中設(shè)置的斷點(diǎn)(0x080483c7)中斷時(shí),test 堆棧幀中就有了p_eip 和 p_ebp 兩個(gè)數(shù)據(jù)。
(gdb) x/2w $esp
0xbfcb3c48: 0xbfcb3c78 0x08048408
分別保存了 main ebp 和 main call 后一條指令的 eip 地址。其后的指令直接修改 [ebp+0xc] 內(nèi)容,使其指向新的字符串 "Ubuntu"。然后累加 [ebp+0x8] 的值,并用 eax 寄存器返回函數(shù)結(jié)果。
0x080483c7 <test+3>: mov DWORD PTR [ebp+0xc],0x80484f0
0x080483ce <test+10>: add DWORD PTR [ebp+0x8],0x1
0x080483d2 <test+14>: mov eax,DWORD PTR [ebp+0x8]
注意都是直接對 main 棧幀中的復(fù)制變量進(jìn)?行操作,并沒有在 test 棧幀中開辟存儲區(qū)域。
(gdb) x/s 0x80484f0
0x80484f0: "Ubuntu!"
執(zhí)行到函數(shù)結(jié)束,然后再次輸出 main 堆棧幀的內(nèi)容看看。
(gdb) finish ! ! ! # test 執(zhí)?行結(jié)束,回到 main frame。
Run till exit from #0 test (x=4660, s=0x80484f8 "Hello, World!") at hello.c:5
0x08048408 in main () at hello.c:14
14 int c = test(x, s);
Value returned is $21 = 4661
(gdb) p $eip ! ! ! # eip 重新指向 main 中的指令
$22 = (void (*)()) 0x8048408 <main+49>
(gdb) x/12xw $esp ! ! # 查看 main 堆棧幀內(nèi)存
0xbfcb3c50: 0x00001235 0x080484f0 0xbfcb3c68 0x080482c4
0xbfcb3c60: 0xb8081ff4 0x08049ff4 0xbfcb3c88 0x00001234
0xbfcb3c70: 0x080484f8 0xbfcb3c90 0xbfcb3ce8 0xb7f39775
重新查看 main 堆棧幀信息,我們會發(fā)現(xiàn)棧頂兩個(gè)復(fù)制變量的值被改變。
(3) 繼續(xù)執(zhí)行,查看修改后的變量對后續(xù)代碼的影響。
當(dāng) call test 發(fā)生后,其返回值 eax 被保存到 [ebp-0x10] 處,也就是變量 c 的內(nèi)容。
繼續(xù) "printf(s)",我們會發(fā)現(xiàn)和 call test 一樣,再次復(fù)制了變量 s 到 [esp]。
0x0804840b <main+52>: mov eax,DWORD PTR [ebp-0x8]
0x0804840e <main+55>: mov DWORD PTR [esp],eax
0x08048411 <main+58>: call 0x80482f8 <printf@plt>
很顯然,這會覆蓋 test 修改的值。我們在 0x08048411 設(shè)置斷點(diǎn),查看堆棧幀的變化。
(gdb) b *0x08048411
Breakpoint 4 at 0x8048411: file hello.c, line 15.
(gdb) c
Continuing.
Breakpoint 4, 0x08048411 in main () at hello.c:15
15 printf(s);
(gdb) x/12w $esp
0xbfcb3c50: 0x080484f8 0x080484f0 0xbfcb3c68 0x080482c4
0xbfcb3c60: 0xb8081ff4 0x08049ff4 0x00001235 0x00001234
0xbfcb3c70: 0x080484f8 0xbfcb3c90 0xbfcb3ce8 0xb7f39775
從輸出的棧內(nèi)存可以看出,[esp] 和 [ebp-0x8] 值相同,都是指向 "Hello, World!" 的地址。
由此可見,test 的修改并沒有對后續(xù)調(diào)用造成影響。這也是所謂 "指針本身也是按值傳送" 的規(guī)則。剩余的工作就是恢復(fù)現(xiàn)場等等,在此就不多說廢話了。
更多建議: