[心得] 用 extern 誤導編譯器,及用VC看組譯碼
看板C_and_CPP (C/C++)作者zlw (www.eJob.gov.tw)時間16年前 (2009/06/19 09:28)推噓5(5推 0噓 8→)留言13則, 5人參與討論串1/2 (看更多)
2009/06/20 AM 05:48 「組語註」部份補充:lea 指令
2009/06/20 AM 04:01 名詞修正:原文提到 dereference 是我一時沒想清楚,容易混淆
以 CPU 的觀點,變數名稱 arr 只是代表一個記憶體位址,比如 0x12345678,從這位址
取出內容值,比較好一點的說法是 CPU 對 arr 做 Load Memory (to register)。以下
簡寫為 LM。
--
假設在檔案 1.cpp 定義初始化 int arr[2] = {100,200};
在另一個檔案 src.cpp 用 extern 連結此陣列時,可以欺騙 compiler 做出錯誤操作。
#include <stdio.h>
extern int *arr;
int main(void){
printf("arr = %d\n", arr); //印出100,而非位址
printf("arr[0] = %d\n", arr[0]); //Segmentation fault (存取違規)
return 0;
}
此例,當要把 arr 當作參數傳給 printf 時,編譯器會用以下指令來取 arr 的值:
mov eax,dword ptr [arr]
mov ecx,dword ptr [eax]
也就是先把 arr 存的值當做「位址」丟給 eax,再從 eax 做 「LM」 把值給 ecx
但是把 arr 存的值 (第一個值) 100 當記憶體位址不是我們要的,所以才會存取錯誤。
--
組語註:對 masm 組合語言來說,符號加上 [] 就是做「LM」,對 nasm 來說亦同。
如果要得到符號代表的記憶體位址,也就是 C/C++ 裡的 &arr,
那 masm 就要用 offser arr 來表示。
但 nasm 是採用直接寫 arr 去掉 [] 來代表 &arr。
另外在VC下中斷點後,就可按右鍵選「移至反組譯碼」跳到組合語言視窗。
裡面寫到符號比如 arr 時,會在旁邊標他的 offset arr 值,如 arr (1234h)。
# 額外注意的是,像這樣一個符號旁邊旁標了 offser 位址,表示可在
程式執行前就知道其記憶體位址 (當然 relocation 是沒有的)。換言之,因為
arr 不是產生在堆疊裡的變數,所以編譯器在產生執行檔前就能決定其數值。
當有一個區域變數比如 void foo(void){int a = -1;} 時,我們沒有辦法事先
知道記憶體位址 (&a),沒有辦法用 offser a 獲得。因為函數 foo() 在被呼叫
之前的 ESP 值是未知的,要到 Runtime 了才可決定。因此要獲得區域變數
的記憶體位址,應改用另外一個指令 lea eax, [a] 去將區域變數 a 的位址在
Runtime 時由 CPU 去計算出來。(Load Effective Address)
注意 a 要加上 [] 才正確。比如自行在 C 裡寫 __asm{ lea eax,x } 還是會被
編譯器把你那行改成 lea eax,[x] 來執行 (至少 VC 測試過是這樣)。
按 ctrl+g 後輸入位址可以跳到該位址去,比如跳去看副程式,或觀看資料。
alt+5 可叫出暫存器視窗。alt+6 可叫出記憶體視窗,比如在上面打 ESP,然後
選右邊的「自動重新評估」按鈕,就可以持續追蹤堆疊的記憶體內容。
當然基本的指令追蹤快速鍵還是跟以前一樣 F10、F11(會跳進去副程式追蹤)
--
當程式碼如下時,並不會有錯誤
int arr2[2]={0,0};
int *ptr = arr;
printf("val = %d", ptr[0]);
「= 運算子」一定先會等右邊的 arr 取出值後,才把該值傳給「另外一個變數」(ptr)
我們知道陣列名稱跟其他變數都不一樣,對陣列名稱取值是得到「所在位址」
若有 int val = 3; 對 val 取值是得到 「所在位址內的存放值」
那 extern 欺騙了編譯器,告訴編譯器:arr 不是陣列名稱
所以當我們如法炮製,要取值給「另外一個變數」ptr 時,就會取出錯誤數據。
一般沒有辦法對變數做重新解讀,只能將變數的值取出後,才用 static_cast
去重新解讀之:int *p = 0; int a = (int)p;
--
利用 extern 可以對全域變數「重新宣告」資料型態一次。但怪就怪在
不能把全域變數 double a = 0; 重新宣告成 extern int a;
卻可以把陣列錯誤的重新宣告成指標,導致潛在的錯誤出現。
當然一般情況都是老實的照著原本的型態宣告而已,不太需要擔心。
不過,在 VC9 的 Release 模式 Build 專案時,有看到以下警告 (Debug 模式無警告)
warning C4743: 'int * arr' 在 1.cpp 及 src.cpp 中有不同的大小: 8 和 4 位元組
warning C4744: 'int * arr' 在 1.cpp 及 src.cpp' 中有不同的型別: 'array (8 bytes
)' 和 'pointer'
--
結論:陣列名稱跟指標我們可以拿來當作等價物使用,但不能讓編譯器把陣列當指標跑。
我們可以不分辨(賺錢),但編譯器不能不分辨。
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 124.8.129.228
推
06/19 09:31, , 1F
06/19 09:31, 1F
→
06/19 09:35, , 2F
06/19 09:35, 2F
推
06/19 10:00, , 3F
06/19 10:00, 3F
→
06/19 10:36, , 4F
06/19 10:36, 4F
推
06/19 11:57, , 5F
06/19 11:57, 5F
查這句話的出處,google 第一頁就看到一篇文章,
剛好就是在講 extern 這個。
google 搜尋 "If you lie to the compiler, it will get its revenge"
第一篇「Software Engineers Toolbox
by Ian Cargill」
節錄如下:
Of course at this stage, you might be saying "Well if the compiler always
makes the corrections, why do I have to bother about the difference?" The
answer is that if you don't know the facts, you will one day lie to the
compiler and, as Henry Spencer said, if you lie to the compiler, it will get
its revenge. When passing arrays or pointers as function parameters it is
pretty difficult to go wrong, but consider the case we had earlier of having
a variable defined as an array in one file, but a declaration in another file
(or in a header file) in the form:
extern char *arry;
This time, think about what the compiler will do when it then encounters a
statement:
c = arry[1];
※ 編輯: zlw 來自: 124.8.129.228 (06/19 12:24)
推
06/19 18:35, , 6F
06/19 18:35, 6F
→
06/19 18:36, , 7F
06/19 18:36, 7F
→
06/19 18:38, , 8F
06/19 18:38, 8F
→
06/19 18:38, , 9F
06/19 18:38, 9F
→
06/19 18:38, , 10F
06/19 18:38, 10F
推
06/20 01:10, , 11F
06/20 01:10, 11F
※ 編輯: zlw 來自: 124.8.129.45 (06/20 04:01)
→
06/20 04:13, , 12F
06/20 04:13, 12F
→
06/20 04:14, , 13F
06/20 04:14, 13F
※ 編輯: zlw 來自: 124.8.129.45 (06/20 05:48)
討論串 (同標題文章)
完整討論串 (本文為第 1 之 2 篇):
C_and_CPP 近期熱門文章
PTT數位生活區 即時熱門文章