鍍金池/ 教程/ C/ 第二部分: 高級
第四部分: 工具
第二部分: 高級
第三部分: 系統(tǒng)
第一部分: 語言

第二部分: 高級

1. 指針概要

簡單羅列一下 C 的指針用法,便于復(fù)習(xí)。

1.1 指針常量

指針常量意指 "類型為指針的常量",初始化后不能被修改,固定指向某個(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 之類的操作,否則會(huì)導(dǎo)致編譯錯(cuò)誤。

1.2 常量指針

常量指針是說 "指向常量數(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 寫在前面更易識(shí)別。

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 的左邊還是右邊。

  • int* const p: const 修飾指針變量 p,指針是常量。
  • int const p: const 修飾指針?biāo)赶虻膬?nèi)容 `p`,是常量的指針?;?qū)懗?const int *p。
  • const int* const p: 指向常量的指針常量。右 const 修飾 p 常量,左 const 表明 *p 為常量。

1.3 指針的指針

指針本身也是內(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 存儲(chǔ)的是指針 p 的地址。因此才有了指針的指針一說。

1.4 數(shù)組指針

默認(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ù)組指針》。

1.5 指針數(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ù)組》。

1.6 函數(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();
    ... ...
}

注意:

  • 定義函數(shù)指針類型: typedef void (inc_t)(int)
  • 定義函數(shù)類型: typedef void (inc_t)(int*)
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;
}

2. 數(shù)組指針

注意下面代碼中指針的區(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 存儲(chǔ)了該數(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ù)組指針類型中有括號(hào),因此 test1 的參數(shù)定義看著有些古怪,不過習(xí)慣就好了。

3. 指針數(shù)組

指針數(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ù)組存儲(chǔ)的都是目標(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] 存儲(chǔ)的指針(&ints[0][0])
    // **p 取出 ints[0][0] 值
    printf("%d\n", **p);

    // -----------------------------------------------

    // p 指向 ints[1]
    p++;

    // *p 取出 ints[1] 存儲(chǔ)的指針(&ints[1][0])
    // **p 取出 ints[1][0] 的值(= 4)
    printf("%d\n", **p);

    // -----------------------------------------------

    // p 指向 ints[2]
    p++;

    // *p 取出 ints[2] 存儲(chǔ)的指針(&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;
}

4. 函數(shù)調(diào)用

先準(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 到堆??臻g預(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í)候的堆棧示意圖如下:

http://wiki.jikexueyuan.com/project/c-study-notes/images/1.png" alt="" />

(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ù)值。

http://wiki.jikexueyuan.com/project/c-study-notes/images/2.png" alt="" />

可以和 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)純粹是為了閱讀代碼方便,本文也以此做示意圖。也就是說在 call test 之前,內(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!";

http://wiki.jikexueyuan.com/project/c-study-notes/images/3.png" alt="" />

main 中的 call 指令會(huì)先將 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 棧幀中開辟存儲(chǔ)區(qū)域。

(gdb) x/s 0x80484f0
0x80484f0: "Ubuntu!"

http://wiki.jikexueyuan.com/project/c-study-notes/images/4.png" alt="" />

執(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 堆棧幀信息,我們會(huì)發(fā)現(xiàn)棧頂兩個(gè)復(fù)制變量的值被改變。

(3) 繼續(xù)執(zhí)行,查看修改后的變量對后續(xù)代碼的影響。

當(dāng) call test 發(fā)生后,其返回值 eax 被保存到 [ebp-0x10] 處,也就是變量 c 的內(nèi)容。

http://wiki.jikexueyuan.com/project/c-study-notes/images/5.png" alt="" />

繼續(xù) "printf(s)",我們會(huì)發(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>

很顯然,這會(huì)覆蓋 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!" 的地址。

http://wiki.jikexueyuan.com/project/c-study-notes/images/1.png" alt="" />

由此可見,test 的修改并沒有對后續(xù)調(diào)用造成影響。這也是所謂 "指針本身也是按值傳送" 的規(guī)則。剩余的工作就是恢復(fù)現(xiàn)場等等,在此就不多說廢話了。