昨天晚上回到寝室,发现室友都没有在玩游戏,而是关灯睡觉了,很是惊奇,后来才发现原来寝室没有交电费给停电了,我倒是觉得挺开心的 -.- ,至少这样可以早睡了。于是也整理睡觉,正要关机的时候收到湖边的短信,让我帮忙调一段很短的汇编代码,其实我汇编已经几乎忘光了,连 mov 的方向在两种汇编下面分别是怎样的都分不清楚了,不过既然是很短的,那还是看看能不能帮上忙吧。于是第二天一大早就起来,然后就邂逅了这个让我不断地以为自己肯定是还没有睡醒的 bug 。代码是嵌入在 C 语言里的,大概是某个题目吧,要求用汇编来实现把小写字母转换为大写,也就是实现注释中那段 C 语言的功能:
#include <conio.h> int ch; void main(void) { _cputs("please input a char: \n"); while ( 1 ) { ch = _getche(); _asm { mov al, ch cmp al, '@' je end1 cmp al,'a' jl end2 cmp al,'z' jg end2 sub al,20h mov ch, al end2: } /*if (ch == '@' ) break; if ( ch >= 'a' && ch <= 'z' ) { ch=ch-0x20; }*/ _putch(c); } end1: ; } |
其实如果你是一个比较细心的人,也许一眼就能看出问题所在了。不过可惜我没那么细心,而且时常会忽略掉一些重要线索,比如上次电脑过热的问题。总之,我看了一下,没发现什么问题,于是决定运行一下试试看,结果嘛:所有的字母都没有转换,而且输入 @ 也没有按照预期的退出。
这个问题是很诡异的,于是我设了个断点,观察 al 的值,发现和我输入的字母简直风马牛不相及,怎么会呢?!我再把断点提前,就在 _getche() 后面断下来,按下 a 之后,程序中断了,此时观察 al 和 ch 的值,竟然都是 0x61 ,也是就是 ‘a’ ,然后我再次确认了一下调试的时候光标所指的这一行是“即将要执行的行”而不是“刚才执行过的行”,看来 _getche() 里面大概用了 al 来用了,所以刚好也是这个值吧。
然而,当我按下 Step 之后……光标跳到下一条语句,说明 mov 指令被执行了,此时在观察,发现 al 变了,只是,变得和 ch 不一样了,至于具体是什么值嘛,那就是天马行空了,几乎每次重新执行都不一样。真是邪门了,赋值之前两个东西还是一样的,赋值之后反而不一样了……我真怀疑自己是不是太惦记着所以现在是梦见自己在调试,确实今天的事情都很不符合常理呀,比如早上 7 点多实验室竟然就有人了,还比如……唔,还有一件比较诡异的事是什么?一时想不起来了,总之,先调程序要紧。
于是我怀疑是不是调用某个函数的时候用到了 al 寄存器,让它被改掉了?于是我把 al 改成了 dl ,运行了下发现结果竟然是一模一样的:赋值之前 dl 居然也等于 ‘a’ ,而赋值之后又变成了奇怪的值!天哪!然后我打开 Visual Studio 的寄存器窗口观察,发现我选 dl 是凑巧,而且凑巧在调用 _getche() 之后 al 和 dl 都等于读入的那个字符,还真是够巧的,我偏偏就选了个 D ……然而更关键的是,赋值变掉的问题还没有解决。而且,这附近就一个 _getche() ,已经调用过了,后面也没有什么函数调用,就一个光棍 mov 语句,还能怎么样?难道 VS 偷偷地插入一些调试相关的代码进去了?于是我不放心地查看了一下生成出来的汇编代码,虽然确实是有一些调试相关的东西(例如 call __RTC_CheckEsp 之类的),不过都离得比较远。这就奇了怪了……
其实答案很简单。我被 Visual Studio 骗了,当然,如果我够细心,我本来早该发现问题的,或者说如果我没有忽略掉那个被我忘掉的诡异的地方的话:就是 ch 是个 int 类型的变量,4 个字节,而 al 是 1 个字节的寄存器,怎么能这样直接 mov 呢?不过我没有仔细想这个问题,大概觉得能自动转换之类的吧,因为脑海里对 x86 汇编的印象就是寻址模式千奇百怪极其复杂。
反正我也不记得我是怎么醒悟的了,不过终于还是醒悟了:ch 明明就是个寄存器嘛。调试的时候,Visual Studio 看到 ch ,就把它当全局变量显示给我看了,值是正确的,没问题;可是,实际执行的是把 ch 寄存器的值赋给了 al 寄存器,于是,是个随机值嘛,一切真相大白了,把变量 ch 改了个名字,一切正常了(当然,这个时候编译器报错了,说 al 的长度和这个 int 型变量不匹配,不能赋值,于是我直接赋给了 eax )。
标题写成这个样子,一方面是为了避免如果标题写得太明显的话,故事讲起来就没有意思了,另一方面,也确实可以顺便说一下,其实这就是名字空间的问题了。在这里,是变量和寄存器两个名字空间,变量的名字和寄存器的名字重复了,也就导致了问题。其他的编程语言里也有类似的问题,通常有“函数”的名字、“变量”的名字、“类型”或者“类”的名字等,例如,在 C 语言里面,类型的名字和变量名应该是属于不同的名字空间里,然而,如果定义某个变量和某个类型名字重复的话,是会出错的;而在 Python 之类的脚本语言里,“类型”本身也就是一个 first-class value ,类型的名字也就和变量名是同样的东西了,压根就在同一名字空间里。
这一方面(就我所知的范围内)最典型的应该还要数 Lisp 的两大变种(或者说方言):Common Lisp 和 Scheme 。前者就是分了两个名字空间:函数和变量,可以定义一个函数 foo 和一个变量 foo ,它们被放在不同的名字空间里,互相不影响;而 Scheme 则坚持一个名字空间,函数和变量都一视同仁。其实想想两种都有它的道理,也不能直接说哪种好哪种坏了。
不过,既然大部分情况还是被放在同一个名字空间里,或者说即使是不同的名字空间也不允许重复,遇到 C 语言之类的情况还会得到编译错误,但是如果是像 Python 之类的动态语言,估计就会 silently fail 了——因为用一个完全不同类型的东西去覆盖一个变量并不是什么不合法的行为呀。比如我最近正在写的一个程序里,有一个 module 的名字叫做 plugin ,在另一个 module 通过 import plugin 就能得到这个 module ,然而我有好几次都差点将一个局部变量命名为 plugin 了,这样就会隐藏掉 plugin module 了。为了避免类似的问题,通常一些变成习惯中也有为不同名字空间的名字选择特定的命名规范的习惯,例如大小写、前缀、后缀之类的。不过,Python (<= 2.6) 的标准库可以说是完美地违反了这类的约定呀:大小写、下划线、CamelCase 被混乱地用在 package 、module 、class 、function 等命名上,标准库看起来一点都不标准。说来也真是邪门,我写 Python 程序应该都写了不少了,可是每次用它的正则表达式库都要去查一遍文档,而且十次有九次都还会出点错才能写对,看来是不是该找一本书来系统地看一下了。 :-/
确实是个很扯的问题。
可是叫“命名空间”问题合适么?这个应该是名字作用域的问题吧。
ch是_asm{}作用域的局部变量名,覆盖了上层作用域对ch的定义而已。应该是scope的问题,而非namespace.
@farawayWind
你说得也有道理,不过也可以说是 C 变量 ch 在汇编里是没有这个概念的,而在 C 里也不能直接通过 ch 这样的名字应用到寄存器,所以也可以看作在不同的“空间”中的。
int 4 byte 32 bit
al ah 8 bit
ax 16 bit
第一次听说还有eax这种32位的寄存器啊,看来16位汇编过时也
受益了 bd
原来当年的无名英雄就是kid.
默默的向让我当年准时提交了那个作业的kid致以崇高的敬意和深深的ym.