Windows记事本编码反汇编分析

转载自:liam.page

网上有一个流传多年的段子。这个段子大致是说,若你在简体中文版本的 Windows 系统下,用系统自带的记事本程序,以默认的 ANSI 编码保存「联通」两个字,那么重新打开后「联通」二字就消失了。如果我没记错的话,还曾有好事者据此编排,认定 Windows 背后的微软和联通有仇,故意不让联通二字正常显示。
当然,这个说法肯定是假的。但是这一现象背后的原因究竟是什么呢?
Windows 记事本都做了什么 这里着重感谢 margen 对 Windows 记事本程序做的逆向工作。没有他的工作,本文不至于这样精彩。光荣属于前辈!
根据 margen 的逆向分析,在打开文件的过程中,记事本程序会调用 fDetermineFileType 来判断文件的编码类型。翻译成 C 语言代码,大致如下。Windows 记事本在以 ANSI 保存文件时,没有任何多余的动作,直接将 buffer 中的内容通过WriteFile 系统调用写入到 txt 文件当中。
我们以 010editor 打开保存了「联通」二字文件看看。
Windows记事本编码反汇编分析
文章图片

可以看到,在简体中文 Windows 下,以记事本保存「联通」两个字。那么保存得到的 txt 文件内,就仅有 0xC1AACDA8 这些内容。而 0xC1AA0xCDA8 正是「联通」两个字的 GBK 编码。
打开的过程 根据 margen 的逆向分析,在打开文件的过程中,记事本程序会调用 fDetermineFileType 来判断文件的编码类型。翻译成 C 语言代码,大致如下。

int __stdcall fDetermineFileType(LPVOID lpBuffer,int cb) { int iType = 0; WORD wSign = 0; if( cb <= 1 ) return 0; wSign = *(PWORD)lpBuffer; switch( wSign ) { case 0xBBEF: { if( cb >= 3 && (PBYTE)lpBuffer[3] == 0xBF) iType = 3; } break; case 0xFEFF: { iType = 1; } break; case 0xFFFE: { iType = 2; } break; default: { if( !IsInputTextUnicode( lpBuffer, cb ) ) { if( IsTextUTF8( lpBuffer, cb ) ) iType = 3; } else iType = 1; } } return iType; }

首先,代码从文件头部取出了前 2 个字节,然后走 switch 分支判断。
若前两个字节是 0xBBEF,且文件第三个字节是 0xBF,则组成 UTF-8 的 BOM(虽然 UTF-8 不需要)。那么据此判断文件编码是 UTF-8。
若前两个字节是 0xFEFF,那么这是小端序 UTF-16 的 BOM。据此判断文件编码是(Windows 所谓的)Unicode 编码。
若前两个字节是 0xFFFE,那么这是大端序的 UTF-16 的 BOM。据此判断文件编码是(Windows 所谓的)Unicode Big Endian 编码。
否则,则需要做更深层次的判断。注意到,iType 被初始化为 0,代表 ANSI 编码(简体中文下是 CP936,相当于是 GBK 编码)。若已走到了 default 分支,要函数返回 0,当且仅当 IsTextUTF8( lpBuffer, cb ) 为 false 才行。然而,这个函数的写法是这样的。
BOOL IsTextUTF8( LPSTR lpBuffer, int iBufSize ) { int iLeftBytes = 0; BOOL bUtf8 = FALSE; if( iBufSize <= 0 ) return FALSE; for( int i=0; i= 0 ) continue; do { c <<= 1; iLeftBytes++; } while( c < 0 ); iLeftBytes--; if( iLeftBytes == 0 ) return FALSE; } else { c &= 0xC0; if( c != (char)0x80 ) return FALSE; else iLeftBytes--; } } if( iLeftBytes ) return FALSE; return bUtf8; }

我们重点看 for循环内部的逻辑。首先,char c = lpBuffer[i]; 从 buffer 中取出一个字节,保存在 signed char 当中。而后判断if( c < 0 )。因为 c 是有符号的 char,所以 c < 0 意味着最高位是 1。这就意味着该字符肯定不是 ASCII 字符,可能是一个 UTF-8 字符。因此将bUtf8 置为 true。
而后,在 if( iLeftBytes == 0 ) 分支中,我们看到 c <<= 1; iLeftBytes++; 的 do-while 循环。这是在判断 UTF-8 编码的首字符中,有多少个前缀的 1。根据 UTF-8 的编码规则,这个数值就是该 UTF-8 字符的编码长度,记录在iLeftBytes 当中。
接下来,根据 iLeftBytes 的大小,逐一检查后续的字节,是否以 10 开头。一旦发现有不满足条件的字节,就能判定当前文档不是 UTF-8 编码的。或是(在 for 循环结束之后)发现 iLeftBytes 尚未自减到 0 就已经到了文档末尾,则也可以判定当前文档不是 UTF-8 编码的。
也就是说,这个函数的逻辑,是根据 UTF-8 编码规则,全文扫描。若发现有一个字符不符合 UTF-8 的编码规则,则返回 false;否则若全文都符合 UTF-8 的编码规则,则返回 true
「联通」都经历了什么? 回过头,我们看到,联通二字以 ANSI(CP936)保存的 txt 文件里只有 0xC1AACDA8这些内容。因为无有 BOM,所以在fDetermineFileType函数中必然走到 default 分支,而后陷入IsTextUTF8 函数当中。
不巧的是,0xC1AA0xCDA8 都符合 UTF-8 编码的要求。因此该函数返回 true。于是,Windows 记事本打开这一文件时,认定这是一个无 BOM 的 UTF-8 编码的文件。于是按照 UTF-8 编码去解读0xC1AACDA8,那么就乱码了。
还有哪些字符 从前文的分析,我们可以得到结论:如果一个以 ANSI(CP936/GBK)保存的文档,内里包含的所有字符,都不幸满足了 UTF-8 的编码规则。那么这个文档将被 Windows 记事本当做是 UTF-8 编码的文件打开,就会乱码。
由于 GBK 是双字节的编码格式,只可能满足 UTF-8 中对 U+0080U+07FF 编码的格式:110xxxxx, 10xxxxxx。我们可以将这些字符全都扫描输出出来。以下是完成这一任务的 Python 代码。
pluses = map(lambda i:"+%s" % (hex(i)[2:].upper()), xrange(16)) headline = "%s%s" % (" " * 7, " ".join(pluses)) print headlinefor i in xrange(192, 224): high = hex(i)[2:].upper() for j in xrange(4): low = hex(128 + j * 16)[2:].upper() chars = map(lambda k:(chr(i) + chr(128 + j * 16 + k)).decode("gbk"), xrange(16)) line = u"0x%s%s %s" % (high, low, " ".join(chars)) print line

得到的结果是:
+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0xC080 纮 纴 纻 纼 绖 绤 绬 绹 缊 缐 缞 缷 缹 缻 缼 缽 0xC090 缾 缿 罀 罁 罃 罆 罇 罈 罉 罊 罋 罌 罍 罎 罏 罒 0xC0A0 罓 馈 愧 溃 坤 昆 捆 困 括 扩 廓 阔 垃 拉 喇 蜡 0xC0B0 腊 辣 啦 莱 来 赖 蓝 婪 栏 拦 篮 阑 兰 澜 谰 揽 0xC180 羳 羴 羵 羶 羷 羺 羻 羾 翀 翂 翃 翄 翆 翇 翈 翉 0xC190 翋 翍 翏 翐 翑 習 翓 翖 翗 翙 翚 翛 翜 翝 翞 翢 0xC1A0 翣 痢 立 粒 沥 隶 力 璃 哩 俩 联 莲 连 镰 廉 怜 0xC1B0 涟 帘 敛 脸 链 恋 炼 练 粮 凉 梁 粱 良 两 辆 量 0xC280 聙 聛 聜 聝 聞 聟 聠 聡 聢 聣 聤 聥 聦 聧 聨 聫 0xC290 聬 聭 聮 聯 聰 聲 聳 聴 聵 聶 職 聸 聹 聺 聻 聼 0xC2A0 聽 隆 垄 拢 陇 楼 娄 搂 篓 漏 陋 芦 卢 颅 庐 炉 0xC2B0 掳 卤 虏 鲁 麓 碌 露 路 赂 鹿 潞 禄 录 陆 戮 驴 0xC380 脌 脕 脗 脙 脛 脜 脝 脟 脠 脡 脢 脣 脤 脥 脦 脧 0xC390 脨 脩 脪 脫 脭 脮 脰 脳 脴 脵 脷 脹 脺 脻 脼 脽 0xC3A0 脿 谩 芒 茫 盲 氓 忙 莽 猫 茅 锚 毛 矛 铆 卯 茂 0xC3B0 冒 帽 貌 贸 么 玫 枚 梅 酶 霉 煤 没 眉 媒 镁 每 0xC480 膧 膩 膫 膬 膭 膮 膯 膰 膱 膲 膴 膵 膶 膷 膸 膹 0xC490 膼 膽 膾 膿 臄 臅 臇 臈 臉 臋 臍 臎 臏 臐 臑 臒 0xC4A0 臓 摹 蘑 模 膜 磨 摩 魔 抹 末 莫 墨 默 沫 漠 寞 0xC4B0 陌 谋 牟 某 拇 牡 亩 姆 母 墓 暮 幕 募 慕 木 目 0xC580 艀 艁 艂 艃 艅 艆 艈 艊 艌 艍 艎 艐 艑 艒 艓 艔 0xC590 艕 艖 艗 艙 艛 艜 艝 艞 艠 艡 艢 艣 艤 艥 艦 艧 0xC5A0 艩 拧 泞 牛 扭 钮 纽 脓 浓 农 弄 奴 努 怒 女 暖 0xC5B0 虐 疟 挪 懦 糯 诺 哦 欧 鸥 殴 藕 呕 偶 沤 啪 趴 0xC680 苺 苼 苽 苾 苿 茀 茊 茋 茍 茐 茒 茓 茖 茘 茙 茝 0xC690 茞 茟 茠 茡 茢 茣 茤 茥 茦 茩 茪 茮 茰 茲 茷 茻 0xC6A0 茽 啤 脾 疲 皮 匹 痞 僻 屁 譬 篇 偏 片 骗 飘 漂 0xC6B0 瓢 票 撇 瞥 拼 频 贫 品 聘 乒 坪 苹 萍 平 凭 瓶 0xC780 莯 莵 莻 莾 莿 菂 菃 菄 菆 菈 菉 菋 菍 菎 菐 菑 0xC790 菒 菓 菕 菗 菙 菚 菛 菞 菢 菣 菤 菦 菧 菨 菫 菬 0xC7A0 菭 恰 洽 牵 扦 钎 铅 千 迁 签 仟 谦 乾 黔 钱 钳 0xC7B0 前 潜 遣 浅 谴 堑 嵌 欠 歉 枪 呛 腔 羌 墙 蔷 强 0xC880 葊 葋 葌 葍 葎 葏 葐 葒 葓 葔 葕 葖 葘 葝 葞 葟 0xC890 葠 葢 葤 葥 葦 葧 葨 葪 葮 葯 葰 葲 葴 葷 葹 葻 0xC8A0 葼 取 娶 龋 趣 去 圈 颧 权 醛 泉 全 痊 拳 犬 券 0xC8B0 劝 缺 炔 瘸 却 鹊 榷 确 雀 裙 群 然 燃 冉 染 瓤 0xC980 蓘 蓙 蓚 蓛 蓜 蓞 蓡 蓢 蓤 蓧 蓨 蓩 蓪 蓫 蓭 蓮 0xC990 蓯 蓱 蓲 蓳 蓴 蓵 蓶 蓷 蓸 蓹 蓺 蓻 蓽 蓾 蔀 蔁 0xC9A0 蔂 伞 散 桑 嗓 丧 搔 骚 扫 嫂 瑟 色 涩 森 僧 莎 0xC9B0 砂 杀 刹 沙 纱 傻 啥 煞 筛 晒 珊 苫 杉 山 删 煽 0xCA80 蕗 蕘 蕚 蕛 蕜 蕝 蕟 蕠 蕡 蕢 蕣 蕥 蕦 蕧 蕩 蕪 0xCA90 蕫 蕬 蕭 蕮 蕯 蕰 蕱 蕳 蕵 蕶 蕷 蕸 蕼 蕽 蕿 薀 0xCAA0 薁 省 盛 剩 胜 圣 师 失 狮 施 湿 诗 尸 虱 十 石 0xCAB0 拾 时 什 食 蚀 实 识 史 矢 使 屎 驶 始 式 示 士 0xCB80 藔 藖 藗 藘 藙 藚 藛 藝 藞 藟 藠 藡 藢 藣 藥 藦 0xCB90 藧 藨 藪 藫 藬 藭 藮 藯 藰 藱 藲 藳 藴 藵 藶 藷 0xCBA0 藸 恕 刷 耍 摔 衰 甩 帅 栓 拴 霜 双 爽 谁 水 睡 0xCBB0 税 吮 瞬 顺 舜 说 硕 朔 烁 斯 撕 嘶 思 私 司 丝 0xCC80 虁 虂 虃 虄 虅 虆 虇 虈 虉 虊 虋 虌 虒 虓 處 虖 0xCC90 虗 虘 虙 虛 虜 虝 號 虠 虡 虣 虤 虥 虦 虧 虨 虩 0xCCA0 虪 獭 挞 蹋 踏 胎 苔 抬 台 泰 酞 太 态 汰 坍 摊 0xCCB0 贪 瘫 滩 坛 檀 痰 潭 谭 谈 坦 毯 袒 碳 探 叹 炭 0xCD80 蛝 蛠 蛡 蛢 蛣 蛥 蛦 蛧 蛨 蛪 蛫 蛬 蛯 蛵 蛶 蛷 0xCD90 蛺 蛻 蛼 蛽 蛿 蜁 蜄 蜅 蜆 蜋 蜌 蜎 蜏 蜐 蜑 蜔 0xCDA0 蜖 汀 廷 停 亭 庭 挺 艇 通 桐 酮 瞳 同 铜 彤 童 0xCDB0 桶 捅 筒 统 痛 偷 投 头 透 凸 秃 突 图 徒 途 涂 0xCE80 蝷 蝸 蝹 蝺 蝿 螀 螁 螄 螆 螇 螉 螊 螌 螎 螏 螐 0xCE90 螑 螒 螔 螕 螖 螘 螙 螚 螛 螜 螝 螞 螠 螡 螢 螣 0xCEA0 螤 巍 微 危 韦 违 桅 围 唯 惟 为 潍 维 苇 萎 委 0xCEB0 伟 伪 尾 纬 未 蔚 味 畏 胃 喂 魏 位 渭 谓 尉 慰 0xCF80 蟺 蟻 蟼 蟽 蟿 蠀 蠁 蠂 蠄 蠅 蠆 蠇 蠈 蠉 蠋 蠌 0xCF90 蠍 蠎 蠏 蠐 蠑 蠒 蠔 蠗 蠘 蠙 蠚 蠜 蠝 蠞 蠟 蠠 0xCFA0 蠣 稀 息 希 悉 膝 夕 惜 熄 烯 溪 汐 犀 檄 袭 席 0xCFB0 习 媳 喜 铣 洗 系 隙 戏 细 瞎 虾 匣 霞 辖 暇 峡 0xD080 衻 衼 袀 袃 袆 袇 袉 袊 袌 袎 袏 袐 袑 袓 袔 袕 0xD090 袗 袘 袙 袚 袛 袝 袞 袟 袠 袡 袣 袥 袦 袧 袨 袩 0xD0A0 袪 小 孝 校 肖 啸 笑 效 楔 些 歇 蝎 鞋 协 挟 携 0xD0B0 邪 斜 胁 谐 写 械 卸 蟹 懈 泄 泻 谢 屑 薪 芯 锌 0xD180 褉 褋 褌 褍 褎 褏 褑 褔 褕 褖 褗 褘 褜 褝 褞 褟 0xD190 褠 褢 褣 褤 褦 褧 褨 褩 褬 褭 褮 褯 褱 褲 褳 褵 0xD1A0 褷 选 癣 眩 绚 靴 薛 学 穴 雪 血 勋 熏 循 旬 询 0xD1B0 寻 驯 巡 殉 汛 训 讯 逊 迅 压 押 鸦 鸭 呀 丫 芽 0xD280 襽 襾 覀 覂 覄 覅 覇 覈 覉 覊 見 覌 覍 覎 規 覐 0xD290 覑 覒 覓 覔 覕 視 覗 覘 覙 覚 覛 覜 覝 覞 覟 覠 0xD2A0 覡 摇 尧 遥 窑 谣 姚 咬 舀 药 要 耀 椰 噎 耶 爷 0xD2B0 野 冶 也 页 掖 业 叶 曳 腋 夜 液 一 壹 医 揖 铱 0xD380 觻 觼 觽 觾 觿 訁 訂 訃 訄 訅 訆 計 訉 訊 訋 訌 0xD390 訍 討 訏 訐 訑 訒 訓 訔 訕 訖 託 記 訙 訚 訛 訜 0xD3A0 訝 印 英 樱 婴 鹰 应 缨 莹 萤 营 荧 蝇 迎 赢 盈 0xD3B0 影 颖 硬 映 哟 拥 佣 臃 痈 庸 雍 踊 蛹 咏 泳 涌 0xD480 詟 詠 詡 詢 詣 詤 詥 試 詧 詨 詩 詪 詫 詬 詭 詮 0xD490 詯 詰 話 該 詳 詴 詵 詶 詷 詸 詺 詻 詼 詽 詾 詿 0xD4A0 誀 浴 寓 裕 预 豫 驭 鸳 渊 冤 元 垣 袁 原 援 辕 0xD6A0 譅 帧 症 郑 证 芝 枝 支 吱 蜘 知 肢 脂 汁 之 织 0xD6B0 职 直 植 殖 执 值 侄 址 指 止 趾 只 旨 纸 志 挚 0xD780 讇 讈 讉 變 讋 讌 讍 讎 讏 讐 讑 讒 讓 讔 讕 讖 0xD790 讗 讘 讙 讚 讛 讜 讝 讞 讟 讬 讱 讻 诇 诐 诪 谉 0xD7A0 谞 住 注 祝 驻 抓 爪 拽 专 砖 转 撰 赚 篆 桩 庄 0xD7B0 装 妆 撞 壮 状 椎 锥 追 赘 坠 缀 谆 准 捉 拙 卓 0xD880 貈 貋 貍 貎 貏 貐 貑 貒 貓 貕 貖 貗 貙 貚 貛 貜 0xD890 貝 貞 貟 負 財 貢 貣 貤 貥 貦 貧 貨 販 貪 貫 責 0xD8A0 貭 亍 丌 兀 丐 廿 卅 丕 亘 丞 鬲 孬 噩 丨 禺 丿 0xD8B0 匕 乇 夭 爻 卮 氐 囟 胤 馗 毓 睾 鼗 丶 亟 鼐 乜 0xD980 賭 賮 賯 賰 賱 賲 賳 賴 賵 賶 賷 賸 賹 賺 賻 購 0xD990 賽 賾 賿 贀 贁 贂 贃 贄 贅 贆 贇 贈 贉 贊 贋 贌 0xD9A0 贍 佟 佗 伲 伽 佶 佴 侑 侉 侃 侏 佾 佻 侪 佼 侬 0xD9B0 侔 俦 俨 俪 俅 俚 俣 俜 俑 俟 俸 倩 偌 俳 倬 倏 0xDA80 趢 趤 趥 趦 趧 趨 趩 趪 趫 趬 趭 趮 趯 趰 趲 趶 0xDA90 趷 趹 趻 趽 跀 跁 跂 跅 跇 跈 跉 跊 跍 跐 跒 跓 0xDAA0 跔 凇 冖 冢 冥 讠 讦 讧 讪 讴 讵 讷 诂 诃 诋 诏 0xDAB0 诎 诒 诓 诔 诖 诘 诙 诜 诟 诠 诤 诨 诩 诮 诰 诳 0xDB80 踿 蹃 蹅 蹆 蹌 蹍 蹎 蹏 蹐 蹓 蹔 蹕 蹖 蹗 蹘 蹚 0xDB90 蹛 蹜 蹝 蹞 蹟 蹠 蹡 蹢 蹣 蹤 蹥 蹧 蹨 蹪 蹫 蹮 0xDBA0 蹱 邸 邰 郏 郅 邾 郐 郄 郇 郓 郦 郢 郜 郗 郛 郫 0xDBB0 郯 郾 鄄 鄢 鄞 鄣 鄱 鄯 鄹 酃 酆 刍 奂 劢 劬 劭 0xDC80 軃 軄 軅 軆 軇 軈 軉 車 軋 軌 軍 軏 軐 軑 軒 軓 0xDC90 軔 軕 軖 軗 軘 軙 軚 軛 軜 軝 軞 軟 軠 軡 転 軣 0xDCA0 軤 堋 堍 埽 埭 堀 堞 堙 塄 堠 塥 塬 墁 墉 墚 墀 0xDCB0 馨 鼙 懿 艹 艽 艿 芏 芊 芨 芄 芎 芑 芗 芙 芫 芸 0xDD80 輤 輥 輦 輧 輨 輩 輪 輫 輬 輭 輮 輯 輰 輱 輲 輳 0xDD90 輴 輵 輶 輷 輸 輹 輺 輻 輼 輽 輾 輿 轀 轁 轂 轃 0xDDA0 轄 荨 茛 荩 荬 荪 荭 荮 莰 荸 莳 莴 莠 莪 莓 莜 0xDDB0 莅 荼 莶 莩 荽 莸 荻 莘 莞 莨 莺 莼 菁 萁 菥 菘 0xDE80 迉 迊 迋 迌 迍 迏 迒 迖 迗 迚 迠 迡 迣 迧 迬 迯 0xDE90 迱 迲 迴 迵 迶 迺 迻 迼 迾 迿 逇 逈 逌 逎 逓 逕 0xDEA0 逘 蕖 蔻 蓿 蓼 蕙 蕈 蕨 蕤 蕞 蕺 瞢 蕃 蕲 蕻 薤 0xDEB0 薨 薇 薏 蕹 薮 薜 薅 薹 薷 薰 藓 藁 藜 藿 蘧 蘅 0xDF80 還 邅 邆 邇 邉 邊 邌 邍 邎 邏 邐 邒 邔 邖 邘 邚 0xDF90 邜 邞 邟 邠 邤 邥 邧 邨 邩 邫 邭 邲 邷 邼 邽 邿 0xDFA0 郀 摺 撷 撸 撙 撺 擀 擐 擗 擤 擢 攉 攥 攮 弋 忒 0xDFB0 甙 弑 卟 叱 叽 叩 叨 叻 吒 吖 吆 呋 呒 呓 呔 呖

若一个文档里只包含这个表格中的汉字(可以再包含 ASCII 字符),在简体中文 Windows 下在记事本中以 ANSI 编码保存,则再次打开必然乱码。特别地,这个表格本身也会乱码。
【Windows记事本编码反汇编分析】据此,你可以构造出各种跟微软「有仇」的文档。比如「联通」,比如「小泉水」。

    推荐阅读