作者zlw (www.eJob.gov.tw)
看板C_and_CPP
标题[心得] 用 extern 误导编译器,及用VC看组译码
时间Fri Jun 19 09:28:05 2009
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
1F:推 VictorTom:小弟先推前一句: 如果你欺骗 compiler, 06/19 09:31
2F:→ zlw:我忘了下一句是什麽了... 06/19 09:35
3F:推 VictorTom:好像是: compiler会向你复仇....XD 06/19 10:00
4F:→ zlw:了解 06/19 10:36
5F:推 herman602:if you lie complier, you will get its revenge. 06/19 11:57
查这句话的出处,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)
6F:推 duidae:array跟pointer不是同一个东西 不过大部分人都会搞混 06/19 18:35
7F:→ duidae:expert C programming这本书不错 有提到元po这个问题 06/19 18:36
8F:→ zlw:没有看过,有机会看看,谢谢。另外我修正一下,错误的extern 06/19 18:38
9F:→ zlw:本来就会让编译器输出错的指令。只是後面看能不能再骗过linker 06/19 18:38
10F:→ zlw:的把关而已。 06/19 18:38
11F:推 softwind:这根本不可能过 arr linker一定报错 找不到ref. 06/20 01:10
※ 编辑: zlw 来自: 124.8.129.45 (06/20 04:01)
12F:→ zlw:gcc方面跟cole945前辈讲的应该一样,可以正常输出执行档。而VC 06/20 04:13
13F:→ zlw:用 cl /TC src.cpp 1.cpp 强制编译成C後还是可以任意extern的 06/20 04:14
※ 编辑: zlw 来自: 124.8.129.45 (06/20 05:48)