前言:指針!菜鳥的終點,高手的起點。漫談一些進階之路上的趣事;記錄一些語言本身的特性以及思想,沒有STL,也沒有API!
0x01: 程序內存中的存儲划分
對於程序在內存中是如何分布的,網上有多個解釋的版本(解釋為3、4、5、6個區的都有),這里我也不贅述了,反正該有的都有,只是看個人怎么理解
建議自己搜來看看溫習一下(主要是棧區、常量區、代碼段),看懵了就不要說我描述有問題了......
0x02: 變量與常量
程序的運行過程(屏蔽一些較為底層的東西):
① 將物理內存(磁盤等存儲介質)中的程序文件裝入運行內存中,程序中的內存指的是運行內存
② CPU從內存中的指定位置讀取到指令加以運行,這里的指令最終都是對於內存的操作
程序中定義的操作存儲於內存 - 代碼段,操作指C代碼指令編譯的結果,例如賦值操作、比較運算等;CPU讀取指令的位置
程序中定義的局部變量存儲於內存 - 棧區,這是我們最常使用的存儲區域;這些變量在同一作用范圍內時我們可以隨意改變其值
程序中定義的常量存儲於內存 - 數據區,數據區中全局變量、靜態變量、常量的存儲區不同,我們通常使用 'const' 定義的常量是存儲在常量區的,常量區的數據根據規定是不可改變的
思考:程序加載到內存中的絕對位置是由操作系統決定的,程序可以加載到的內存(除系統保留區)也是平等的,為什么存儲在棧區的變量可以改變而存儲在代碼區和常量區的數據不可改變;理論上來說該程序可以操作的內存(也就是系統加載該程序時分配的內存地址范圍)都是可以被改變的,所以這里可以推測為程序做了權限的限制
0x03: 指針操作的本質
指針操作是可以直接作用於內存的,使用指針操作時只有兩個限制,一個是定義指針時規定的對於變量本身的限制,一個是該程序的尋址空間限制;在某些情況下這兩個限制都可以突破,這里不作論述
指針的強大之處在於它能修改所有能尋址到的內存中的值;對應程序在內存中的分配,理論上可以使用指針操作棧區堆區(常用),那么同樣可以操作數據區和代碼區;語言限制中不允許修改操作的區域為代碼區和數據區中的常量區,這里我們可以將指針指向這兩個區域,這樣就能達到修改代碼和常量的目的
0x04: 通過指針操作常量區
代碼示例:

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時就碰到了這個問題,當時問老師說沒遇到過我這么用的,就沒有和這個問題剛到底(也不會這些技術);技術的話可能還是有一些地方描述得有問題,望大佬不吝賜教,同時也希望這篇文章中的東西能對同學們有哪怕一絲用處