1. 程式人生 > >C語言之修改常量

C語言之修改常量

  前言:指標!菜鳥的終點,高手的起點。漫談一些進階之路上的趣事;記錄一些語言本身的特性以及思想,沒有STL,也沒有API!

0x01: 程式記憶體中的儲存劃分

  對於程式在記憶體中是如何分佈的,網上有多個解釋的版本(解釋為3、4、5、6個區的都有),這裡我也不贅述了,反正該有的都有,只是看個人怎麼理解

  建議自己搜來看看溫習一下(主要是棧區、常量區、程式碼段),看懵了就不要說我描述有問題了......

0x22: 變數與常量

  程式的執行過程(遮蔽一些較為底層的東西):

    ① 將實體記憶體(磁碟等儲存介質)中的程式檔案裝入執行記憶體中,程式中的記憶體指的是執行記憶體

    ② CPU從記憶體中的指定位置讀取到指令加以執行,這裡的指令最終都是對於記憶體的操作

  程式中定義的操作儲存於記憶體 - 程式碼段,操作指C程式碼指令編譯的結果,例如賦值操作、比較運算等;CPU讀取指令的位置

  程式中定義的區域性變數儲存於記憶體 - 棧區,這是我們最常使用的儲存區域;這些變數在同一作用範圍內時我們可以隨意改變其值

  程式中定義的常量儲存於記憶體 - 資料區,資料區中全域性變數、靜態變數、常量的儲存區不同,我們通常使用 'const' 定義的常量是儲存在常量區的,常量區的資料根據規定是不可改變的

  思考:程式載入到記憶體中的絕對位置是由作業系統決定的,程式可以載入到的記憶體(除系統保留區)也是平等的,為什麼儲存在棧區的變數可以改變而儲存在程式碼區和常量區的資料不可改變;理論上來說該程式可以操作的記憶體(也就是系統載入該程式時分配的記憶體地址範圍)都是可以被改變的,所以這裡可以推測為程式做了許可權的限制

0x32: 指標操作的本質

  指標操作是可以直接作用於記憶體的,使用指標操作時只有兩個限制,一個是定義指標時規定的對於變數本身的限制,一個是該程式的定址空間限制;在某些情況下這兩個限制都可以突破,這裡不作論述

  指標的強大之處在於它能修改所有能定址到的記憶體中的值;對應程式在記憶體中的分配,理論上可以使用指標操作棧區堆區(常用),那麼同樣可以操作資料區和程式碼區;語言限制中不允許修改操作的區域為程式碼區和資料區中的常量區,這裡我們可以將指標指向這兩個區域,這樣就能達到修改程式碼和常量的目的

0x42: 通過指標操作常量區

  程式碼示例:

const int a=10;
int *pa=(int*)&a;
*pa=99;
printf("*pa=%d,a=%d",*pa,a);

/*輸出:
*pa=99,a=10
*/
常量修改

  示例中第二行必需使用強轉,C中認為 'const' 是更加廣泛的型別限制

  輸出結果是不是有點奇怪?理論上來說定義的 'const' 常量儲存的常量區也在記憶體中,為什麼 'a' 和 '*pa' 的值不一樣呢?難道說使用這兩個名的時候不是定址的同一塊記憶體?或者是程式定址的時候使用相同的地址實際地址是不同的區域(a在常量區,對pa賦值時在棧區生成了新的*pa記憶體)?

  我們再深入看看:

const int a=10;
int *pa=(int*)&a;
printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);
*pa=99;
printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);

/*輸出:
*pa=10,a=10,pa=0019FF3C,&a=0019FF3C
*pa=99,a=10,pa=0019FF3C,&a=0019FF3C
*/
常量地址

  這裡分別輸出了賦值之前兩者的值和地址、賦值之後兩者的值和地址,這裡我們可以知道地址是相同的,但值就是不同???我去 哪有這麼怪的事,同一塊地址的值同一時間取怎麼就不同了?

  這時候我們使用 'F10' 單步除錯大法進行變數跟蹤(VC++6.0),開啟變數池、記憶體,跟蹤過程(需要一點點的除錯能力):

    ① 第一行定義 'const' 變數,檢視變數 'a' 的值(=10)、檢視 'a' 的地址 '&a' (=0x0019FF3C)

    ② 第二行定義 'int' 指標指向 'a',檢視 '*pa' 的值(=10)、檢視 'pa' 的值(=0x0019FF3C)

    ③ 執行並檢視輸出,沒問題

    ④ 使用 '*pa' 對這塊記憶體賦值,檢視 'a'、'&a'、'*pa'、'pa' 的值,其中 'a' 的值和 '*pa' 的值變成了 '99',正常

    ⑤ 執行並檢視輸出,得到輸出中的最後一行 '*pa=99,a=10,pa=0019FF3C,&a=0019FF3C'

  ???啥意思???④中得到了 'a' 的值明明為 '99',⑤這輸出咋回事兒啊?

  再使用記憶體view檢視地址為 '0x0019FF3C' 地址內的值,為 '63 00 00 00',小端儲存的十六進位制,63H==99D;可以得到的結論為:使用這兩個名進行定址的是同一塊記憶體,同一程式中定址方式唯一

  問題就在於這一塊記憶體的原值在賦值之後已經被新的值覆蓋掉了,讀取到的 'a' 的值是從哪來的,'a' 的值一定在記憶體中的某個位置

  接下來再進一步跟蹤程式執行過程,查詢程式中間步驟,單步除錯彙編語句,開啟暫存器、彙編檔案(需要再多一點點除錯能力,只解釋相關語句):

1:    #include <stdio.h>
2:
3:    void main(void)
4:    {
00401010   push        ebp
00401011   mov         ebp,esp
00401013   sub         esp,48h
00401016   push        ebx
00401017   push        esi
00401018   push        edi
00401019   lea         edi,[ebp-48h]
0040101C   mov         ecx,12h
00401021   mov         eax,0CCCCCCCCh
00401026   rep stos    dword ptr [edi]
5:        const int a=10;
00401028   mov         dword ptr [ebp-4],0Ah
6:        int *pa=(int*)&a;
0040102F   lea         eax,[ebp-4]
00401032   mov         dword ptr [ebp-8],eax
7:        *pa=99;
00401035   mov         ecx,dword ptr [ebp-8]
00401038   mov         dword ptr [ecx],63h
8:        printf("*pa=%d,a=%d,pa=%p,&a=%p\n",*pa,a,pa,&a);
0040103E   lea         edx,[ebp-4]
00401041   push        edx
00401042   mov         eax,dword ptr [ebp-8]
00401045   push        eax
00401046   push        0Ah
00401048   mov         ecx,dword ptr [ebp-8]
0040104B   mov         edx,dword ptr [ecx]
0040104D   push        edx
0040104E   push        offset string "*pa=%d,a=%d,pa=%p,&a=%p\n" (0042201c)
00401053   call        printf (00401090)
00401058   add         esp,14h
9:    }
0040105B   pop         edi
0040105C   pop         esi
0040105D   pop         ebx
0040105E   add         esp,48h
00401061   cmp         ebp,esp
00401063   call        __chkesp (00401110)
00401068   mov         esp,ebp
0040106A   pop         ebp
0040106B   ret
彙編檔案

    以上彙編程式碼中的註釋程式碼為C的原始碼,其餘彙編語句只做重要點的講解

    ① 4-5 行之間是做初始化、入棧一類的操作,略過

    ② 5-6 將 0xA 存到 'a'

    ③ 6-7 取 'a' 的地址存到 'pa'

    ④ 7-8 取 'pa' 值對應的地址,存入 0x63

    ⑤ 8-9 輸出:將變數壓棧、字串壓棧呼叫 'printf()' 庫函式

    ⑥ 9-最後 出棧、釋放空間、返回等操作

    其中 ① ⑥ 我也不太懂,②-④步是比較簡單的操作,關鍵在第⑤步

    可以看到 'printf()' 函式的呼叫過程:依次使用 'push' 壓入4個需要串化的引數、壓入原字串,最後 'call printf()',壓入引數的順序為從右至左:

      執行第一個 'push' 時查得 'edx' 值為 0x0019FF3C,是 '&a' 的值

      第二個 'push' 時 'eax' 值為 0x0019FF3C,是 'pa' 的值

      第三個 'push' 的值為 0x0A,是 'a' 的值

      第四個 'push' 時 'edx' 值為 0x63,是 '*pa' 的值

  問題就在於第三個 'push' 目標直接為值 0x0A 而不是取 '&a' 這個地址內的值,根據之前的推斷即使是常量也需要從其儲存位置取值,而實際情況卻是在編譯時就進行了類似 '#define' 之類的直接替換......

章結:

  一個無聊的實驗,如何修改常量;得出的結論:使用指標操作常量區是沒有任何問題的,但有時即使修改了常量區的值也對執行結果沒有影響,編譯器會優化在使用常量時不去常量的儲存位置取值,而是編譯階段直接將值寫入到程式碼區

  另:即使寫入到程式碼區的值也可以修改,通過某種神奇的方法找到編譯後代碼的位置,將邏輯修改為從記憶體尋值;或者暴力點內嵌彙編......

 寫在最後:

  這是一個困擾了我三年的問題(有點丟人),初學C時就碰到了這個問題,當時問老師說沒遇到過我這麼用的,就沒有和這個問題剛到底(也不會這些技術);技術的話可能還是有一些地方描述得有問題,望大佬不吝賜教,同時也希望這篇文章中的東西能對同學們有哪怕一絲用處

&n