分享Wine调试经验 -- 第二季: Wine Dr.com 中文乱码

986 views
Skip to first unread message

Qian Hong

unread,
Aug 23, 2012, 4:51:52 AM8/23/12
to gz...@googlegroups.com
Hi all,

最近修复了Wine的一个bug, 在这里将分析的大概思路记录下来, 既是自己学习的总结, 也希望可以跟大家分享, 吸引更多朋友参与Wine的开发.
之前分享过一次调试Wine QQ崩溃的经历, 不妨称为第一季:
https://groups.google.com/d/topic/gzlug/6boQFg8VO_Y
第一季中的bug, 到现在还没有真正被修复, 不过第二季已经迫不及待上映了 ;-)
这次是带patch的哦~

先简单描述一下bug:
前段时间在研究Wine Dr.com的时候, 偶然发现某个版本的Dr.com在wine下中文显示为乱码.
重现步骤:
1. 下载 Dr.com 3.71 版 [1]
注: 该bug仅在3.71版中能重现
2. 安装Wine, 版本低于或等于 1.5.10
注: 该bug已经在最新的wine 1.5.11中修复了.
3. 解压并启动drcom安装包:
$ wine 校园网拨号客户端3.71.exe
4. 点击 "确定" 和 "下一步", 就可以看到在license窗口中, 中文全部乱码, 摘录部分乱码文字如下:
--- snip ---
锘垮煄甯傜儹鐐规湁闄愬叕鍙镐笌鏈€缁堢敤鎴疯鍙崗璁?
============================

璇疯鐪熼槄璇伙細

鏈崗璁负涓汉鎴栧洟浣撲笌鍩庡競鐑偣鏈夐檺鍏徃涔嬮棿鐨勬湁鍏炽€奃r.COM瀹藉甫璁よ瘉瀹㈡埛绔強Dr.COM ARP
Firewall浜у搧銆嬬殑娉曞緥鍗忚銆傘€奃r.COM瀹藉甫璁よ瘉瀹㈡埛绔強Dr.COM ARP
============================
--- snip ---

可以看到, 除少数英文字符和标点符号可以辨认之外, 其他地方都是乱码, 完全无法猜测其含义.

下面就是调试的思路的经过.

1. 找出Dr.com显示文字所用的函数的调用路径, 更确切的说是找到一段相关的backtrace.

1.0
了解win32 api变成的朋友应该知道, Windows下用于绘制字符的函数包括DrawText, TextOut 等等,
而这些函数都是一些wrapper, 最根本的函数是ExtTextOutW().
如果不了解这些, 也没有关系, 只要通过google搜索一下, 就能恶补一下背景知识.
无奖问答: Linux Xlib下, 绘制字符的最根本的函数是什么? 不知道没关系, 赶紧google一下. 如果你能找到答案,
那同样的道理也能通过google找到ExtTextOutW()了.

1.1
为了找出Dr.com是怎么显示字符的, 我的思路是, 用winedbg调试器启动dr.com,
然后在函数ExtTextOutW()处添加断点, 当Dr.com正好要显示乱码的那段文字的时候, 就可以把backtrace打印出来分析.
winedbg的使用方式和gdb非常类似, 感兴趣的朋友可以搜索wine官方的文档, 结合gdb的经验进行尝试.
在这一步, 我一开始被糊弄了, Dr.com总是没有在我期待的断点停下来, 后来才发现, Dr.com的安装包其实是一个wrapper,
它先解压出一个叫XXX.tmp的文件, 然后再启动这个XXX.tmp进行真正的安装过程. 因此, 我们要跟踪调试的其实是这个XXX.tmp,
这可以用winedbg的attach功能实现, 跟gdb的语法类似:

首先, 开第一个终端, 启动Dr.com安装包, 确切的说, 是一个outer installer
$ wine 校园网拨号客户端3.71.exe

然后, 开第二个终端, 运行winedbg, 列出当前运行的进程, 找到我们需要调试的目标, 也就是inner installer
Wine-dbg>info proc /* 打印所有wine进程, 注意 is-813NC.tmp就是我们需要跟踪的进程, 但这个文件名是随机的 */
pid threads parent executable (all id:s are in hex)
0000000e 5 0000000a 'services.exe'
00000012 4 0000000e 'winedevice.exe'
0000001a 3 0000000e 'plugplay.exe'
00000029 1 00000000 'lisence-chinese-font-bug.exe'
0000002b 1 00000029 'explorer.exe'
0000002d 1 00000029 'is-8I3NC.tmp'
Wine-dbg>attach 0x2d /* 跟踪0x2d这个进程 */
0xb776e422 __kernel_vsyscall+0xe in [vdso].so: int $0x80

Wine-dbg>b ExtTextOutW /* 添加断点 */

Wine-dbg>info local /* 打印参数和局部变量 */
注意打印结果的这一行:
LPCWSTR str="选择你在安装过程中需要的语言: ?篃?纤濴纤-" (parameter [ESP+24])
如果对ExtTextOutW的用法有了解, 可以知道str这个参数就是即将显示的字符串. (同样, 如果不了解的话,
等到调试的时候遇到了再google也不迟, 我就是这么做的 :P )
注: 之所以str的末尾有一些乱码字符, 是因为str不是null结尾的, 该函数用另外一个参数来确定str的长度, 所以末尾有垃圾字符是很正常的.

Wine-dbg>display str /* 监视str变量的值 */

接下来, 不断地输入 c ( continue), 直到发现我们之前看到的乱码字串:
Stopped on breakpoint 1 at 0x7e9e48ba ExtTextOutW
[/home/fracting/wine-git/dlls/gdi32/font.c:2074] in gdi32
1: str = "锘垮煄甯傜儹鐐规湁闄愬叕鍙镐笌鏈€缁堢敤鎴疯鍙崗璁?"

这时候, 再输入 bt, 就可以打印出backtrace了:
(仅摘录一部分)
Wine-dbg>bt
Backtrace:
=>0 0x7e9e48ba ExtTextOutW(hdc=0x328, x=0x1, y=0xb, flags=0,
lprect=(nil), str="锘垮煄甯傜儹鐐规湁闄愬叕鍙镐笌鏈€缁堢敤鎴疯鍙崗璁?", count=0x1c,
lpDx=0x0(nil)) [/home/fracting/wine-git/dlls/gdi32/font.c:2074] in
gdi32 (0x0033e39c)
1 0x7d830e13 ME_DrawRun+0x36a(c=0x33f8f4, x=0x1, y=0xb,
rundi=0x5c34e0, para=0x5c2f0c)
[/home/fracting/wine-git/dlls/riched20/paint.c:437] in riched20
(0x0033e41c)
2 0x7d832954 ME_DrawParagraph+0x52b(c=0x33f8f4, paragraph=0x5c2f00)
[/home/fracting/wine-git/dlls/riched20/paint.c:946] in riched20
(0x0033f8cc)
3 0x7d82fc54 ME_PaintContent+0x210(editor=0x5c28e8, hDC=0x328,
rcUpdate=0x33f9f8) [/home/fracting/wine-git/dlls/riched20/paint.c:75]
in riched20 (0x0033f94c)
4 0x7d82dad3 RichEditWndProc_common+0x3fa(hWnd=0x200ba, msg=0xf,
wParam=0, lParam=0, unicode=0)
[/home/fracting/wine-git/dlls/riched20/editor.c:4466] in riched20
(0x0033fa3c)
5 0x7d82de15 RichEditWndProcA+0x2d(hWnd=0x200ba, msg=0xf, wParam=0,
lParam=0) [/home/fracting/wine-git/dlls/riched20/editor.c:4528] in
riched20 (0x0033fa6c)
6 0x7eb2913e WINPROC_wrapper+0x19() in user32 (0x0033fa9c)
7 0x7eb29293 call_window_proc+0xcd(hwnd=0x200ba, msg=0xf, wp=0,
lp=0, result=0x33fb1c, arg=0x7d82dde7)
[/home/fracting/wine-git/dlls/user32/winproc.c:242] in user32
(0x0033faec)


到这里, 我们就完成了第一步.
未完待续, 占位 ;-)

[1] http://wlzx.cdutetc.cn/down.do?action=downLoad&did=2c9082c818e76c2a0118ea364de80002


--
Regards,
Qian Hong

-
Sent from Ubuntu
http://www.ubuntu.com/

Qian Hong

unread,
Aug 23, 2012, 5:30:18 AM8/23/12
to gz...@googlegroups.com
回顾和补充:

- 上一节记录了通过winedbg打印Dr.com绘制字符的关键函数调用的过程.
如果对具体细节感兴趣, 最好的方式就是亲自动手实验一下
- 为了让打印出来的backtrace带有人类可读的符号名, 需要安装调试符号信息. ubuntu下可以安装
wine-1.5-dbg这个包. 当然, 对于开发者或者潜在的开发者来说, 更好的方法是从源代码自己编译, 编译的时候打开 -g 选项.
- 补充几个引用链接:
ExtTextOut函数的说明:
http://msdn.microsoft.com/en-us/library/windows/desktop/dd162713(v=vs.85).aspx
winedbg的用法: http://www.winehq.org/docs/winedev-guide/dbg-commands


好, 接下来我们再进一步, 缩小bug可能出现的范围.

首先, 观察backtrace, 看看能够给我们提供什么信息, 例如这两行:
=>0 0x7e9e48ba ExtTextOutW ...


1 0x7d830e13 ME_DrawRun+0x36a(c=0x33f8f4, x=0x1, y=0xb,
rundi=0x5c34e0, para=0x5c2f0c)
[/home/fracting/wine-git/dlls/riched20/paint.c:437] in riched20
(0x0033e41c)

这里面包含了函数名, 参数名, 源代码文件名, 模块名
从上面两行可以看出, ExtTextOutW这个函数是由 ME_DrawRun这个函数调用的, 而后者属于riched20这个模块.
再往下读backtrace, 都可以看到riched20这个模块, 因此可以断定, Dr.com使用了riched20这个库来显示文字.
同样, 如果对riched20不了解, 可以google一下, 我在调试这个问题的时候, 就对riched20完全不了解.

当我们猜测Wine的一个bug跟某个模块相关的时候, 通常可以试试从Windows下复制一个同名的dll放到Wine的相应目录下,
覆盖掉wine自带的dll, 看看问题是否解决, 如果能解决, 就能立即缩小bug的可能的范围.

幸运的是, `winetricks -q riched20` 可以很方便地帮我们覆盖掉riched20.dll
不幸的是, 我又一次被坑了...
我第一次尝试native riched20的时候, 发现bug没有消除, 这害我走了很多弯路, 直到后来碰巧尝试了自己Windows
XP上的riched20.dll, 又发现bug消除了, 这让我恍然大悟, 原来是版本的问题, 也就是说, 早期的windows版本,
也有某个bug会导致Dr.com乱码, 而这个bug在新版本的Windows里的riched20.dll里修复了.

这里可以给我们一个启发: 报bug/分析问题的时候, 版本号真是一个非常重要的信息. 例如这个bug, 仅在Dr.com 3.71上能重现,
在 3.72, 3.73, 5.2.0上都无法重现. 同时, 这个bug在某些旧版本的riched20下会出现, 在新版本中又不会.

到这里, 我们不妨忽略走弯路的那一部分, 假定我们已经确定这个bug是由riched20引起的, 那么接下来应该怎么做呢?

这时候, 我的思路是这样的:
分两种情况:
最幸运的情况是, bug就出现在我们打印出来的backtrace的大约5个函数中
最不幸的情况是, bug出现在这次backtrace之前的某个未知的函数

如果是第一种情况, 那么只要利用winedbg的 up/down 命令进入到不同的函数栈帧中, 打印相应的局部变量, 一一对比,
就会找到某个字符串变量指向一个不乱码的字符串, 结合源代码分析这个变量是怎么转换成一个乱码的字符串, 就能找到问题所在.
这种情况的工作量不会很大, 只要对backtrace中出现的5个riched20相关的函数一一排查就行了.
不幸的是, 一一排查之后发现, backtrace中的所有字符串都是乱码的, 也就是说, bug出现在这次backtrace的任何调用之前.

肿么办?肿么办? 未完待续, 继续占楼 ;-)

Qian Hong

unread,
Aug 23, 2012, 6:03:27 AM8/23/12
to gz...@googlegroups.com
回顾与补充:

前两节从backtrace入手做了初步分析, 这两部分其实不包含什么Wine特有或Windows特有的背景知识和技巧, 所用到的知识都是可以通用的.

但是, 前面的分析方法已经遇到了瓶颈. 如果是在一个闭源的操作系统上调试一个开源软件或者自己写的软件, 那么我们可以跟踪软件的源代码,
去找到可能出现bug的地方, 一一排除. 然而我们现在要做的是一件相反的事情: 在一个开源的api兼容层上, 调试一个闭源的软件!
这种调试的思维方式会跟传统的调试有微妙的区别, 这也是Wine的开发好玩的地方.

在继续阅读之前, 推荐了解Linux下的strace和ltrace命令. 这两个命令可以用来跟踪闭源软件的系统调用和库函数调用.
有了这两个命令, 实际上闭源软件根本没有秘密可言.

gzlug的bangerlee童鞋分享过strace的教程, 让我很受益, 再次向大家推荐:
http://www.cnblogs.com/bangerlee/archive/2012/02/20/2356818.html
(不过这个链接现在好像无法直接访问了?)

在Wine的开发中, 有一些调试参数可以用来追踪系统调用, 有一些函数可以用了追踪库函数调用.
追踪系统调用的参数是 WINEDEBUG=+relay , 这可能是我这篇文章中第一个无法通过通用的开发常识和google搜索直接找到的知识点.
我自己是在向wine报bug的过程中从开发者那里学到的, 因为常有开发者叫我提供什么+relay的日志,
我就郁闷这个+relay有什么特别之处, 一经尝试才发现这个东西就是strace的windows版.

更多关于wine调试参数的资料, 可以参考这个页面:
http://wiki.winehq.org/DebugChannels

针对我们这个具体的问题, 我们需要先结束掉drcom进程, 然后用如下命令行重新启动drcom, 保存调试日志:
$ WINEDEBUG=+relay,+font,+richedit wine 校园网客户端.exe &> relay-font-rechedit.log

注意这里除了用到 +relay参数, 还用到+font和+richedit, 后两者都是在对wine有了一定了解之后可以从源代码里找到的.
例如, 我们在第一节中提到 ExtTextOutW这个函数, 在wine的源代码中,
借助ctags可以找到这个函数定义的地方是dlls/gdi32/font.c
而在font.c这个文件的前面, 有这么一行:
WINE_DEFAULT_DEBUG_CHANNEL(font);
这就提示我们, 可以通过WINEDEBUG=+font 来获取ExtTextOutW所在文件的所有函数的日志.
同理, 第二节的backtrace中, 可以找到ME_DrawRun等几个函数, 而这几个函数所在的源文件则提示了+richedit 这个调试参数.

用上述命令行启动drcom之后, 点击确定和下一步, 等待乱码的窗口出现, 就已经收集到我们需要的日志,
到这里就可以用Ctrl+C退出程序, 避免运行时间太久日志太长, 给分析带来不必要的麻烦.

在我这里测试的结果是, 保存下来的日志有几十MB之大, 40多万行.
如何从这么多日志里找到有用的信息, 正是wine的开发和调试最刺激最痛苦的事情之一.
其实几十MB的日志不算什么, 读过strace的日志的童鞋一定能体会到, strace的日志那也是相当的长.
同样的, 带+relay调试参数通常也变态长, 几十MB的日志是很少的, 一两G的日志都是很正常的. Wine的一个开发者曾说过,
如果你习惯了阅读几G几G的日志, 那你就离成为一名Wine开发者不远了 ;-)

那么, 究竟如何阅读那么长的日志呢? 肯定需要一些经验和技巧, 不过我的经验也很有限, 捉襟见肘了 ;-)
未完待续, 继续占位~

Qian Hong

unread,
Aug 23, 2012, 7:17:37 AM8/23/12
to gz...@googlegroups.com
回顾:
前三节先用winedbg打印出backtrace, 来确定显示乱码文字的函数的backtrace, 从backtrace中寻找有嫌疑的函数.
尽管没有找到直接引起bug的函数, 但是backtrace中的ExtTextOutW和ME_DrawRun等可以提示我们需要重点观察哪些日志.
我们通过阅读源代码, 可以看到ExtTextOutW对应的调试参数是+font, ME_DrawRun对应的调试参数是+richedit, 从而决定使用
$ WINEDEBUG=+relay,+font,+richedit 来记录日志.
其中+relay相当于linux下的strace, 而+font和+richedit相当于ltrace

接下来的任务就是, 从几十MB的日志中寻找关键的信息, 这里关键的想法就是, 利用不同调试频道的日志互相进行定位. 调试频道就是指DEBUG
CHANNEL, 参见上一节的引用链接.

在正式阅读该死的几十M的日志之前, 不妨先来初步了解一下这些日志的格式.
先观察下面这段随机摘录的日志:
===
001b:Call rpcrt4.NdrGetBuffer(0033f7ec,00000026,00117a08) ret=7ebbbb6a
001b:Call ntdll.RtlAllocateHeap(00110000,00000000,00000026) ret=7eb40d3d
001b:Ret ntdll.RtlAllocateHeap() retval=00117a70 ret=7eb40d3d
001b:Ret rpcrt4.NdrGetBuffer() retval=00117a70 ret=7ebbbb6a
===

这是典型的+relay产生的日志. 其中001b是进程号的十六进制表示, Call 表示函数调用开始, Ret表示函数调用结束, 而括号中的
(0033f7ec, 00000026, 00117a08)等, 表示函数调用时传入的参数. 最后的
ret=7ebbbb6a表示函数的返回地址.

通常Call和Ret都会成对出现, 而不同的{Call,Ret}对与对之间既有可能是顺序关系又有可能是嵌套关系.
顺序关系说明他们是按顺序执行的, 而嵌套关系则说明外面的函数其实是里面的函数的一个wrapper, 例如上面的日志表明,
NdrGetBuffer()这个函数其实是RtlAllocateHeap()的wrapper.

+relay产生的日志, 虽然会打印出函数被调用时传入的参数, 可是却常常是一些莫名奇妙的十六进制数,
如果要重点看一下某个函数参数代表什么意思, 通常可以打开该函数的调试频道重新生成一遍日志, 再结合 +relay, 就能得到更详细的信息,
尤其当参数是指针的时候可能会很有帮助.
而某个函数在某个调试频道下打印什么信息, 其实是由开发者或者由我们自己决定的, 例如,
NdrGetBuffer()函数的源代码中, 有这么一行:
TRACE("(stubmsg == ^%p, buflen == %u, handle == %p)\n", stubmsg,
buflen, handle);

这里的TRACE和printf的语法是相同的, 必要的时候我们也可以自己去修改这些TRACE语句, 比如把指针指向的内存的数据打印出来, 等等.

Qian Hong

unread,
Aug 23, 2012, 8:09:42 AM8/23/12
to gz...@googlegroups.com
接下来, 进入最关键的部分, 从log中查找蛛丝马迹, 尝试定位bug.

首先, 回顾第一节中出现的乱码字符串:"锘垮煄......."
我们要做的是, 从log中找到这串字符.

然而, 直接寻找这段中文字符是无法找到的, 因为wine的日志通常不会直接打印非ASCII字符, 而是尽量用十六进制的形式打印出来.
这里有两种方法去搜索我们需要的字符串, 一种是先查询到 "锘垮煄..."的 Uicode UTF16表示, 然后去搜索相应的十六进制, 另外一种就是,
用"锘垮煄..."这串字符中少数的几个英文单词做为标记, 去找到日志中相应的字符串.
两种方法各有长短, 必要的时候都可以尝试一下.

不管用哪种方法, 我们都能找到这么一行日志:
===
0024:Call gdi32.GetTextExtentPoint32W(00000328,005c45a0
L"\93c8\e100\5d17\7481\e1bb\8d1f\6d93\e043\6c49\93b4\6827\6d1f\6d63\64b2\7b0c\9369\5ea1\7af6\9411\e160\5063\93c8\5910\6aba\934f\e100\5f83\6d94\5b2e\68ff\9428\52ec\6e41\934f\70bd\20ac\5943r.COM\7039\85c9\752b\7481\3088\7609\7039\3221\57db\7ed4\e21a\5f37Dr.COM
ARP Firewall\6d5c\0443\6427\9286\5b2c\6b91\5a09"...,000000ef,0033bd0c)
ret=7d1cf795
===

这里GetTextExtentPoint32W是一个跟ExtTextOutW配套使用的函数, 作用是帮助确定字符应该在窗口的什么位置显示, 显示的大小如何等等.
这也是没有win32开发经验的人能通过google搜到的信息.

我们关心的问题是, L"\93c8\e100\5d17..." 这个字符串是从哪里来的???

假如Wine的输出日志足够完美, 那么我们应该能够从日志中找到类似这么一行:
某某某函数, 输入某某参数, 输出 L"\93c8\e100..."这个字符串

遗憾的是, 找了半天也找不到类似的日志. 在几十M的日志里找这样一句日志, 那真是相当恼火 ;-)
这种情况, 有可能是我们打开的调试频道还不够多, 但是, 在找到有嫌疑或者有价值的函数之前, 又难以预知需要什么调试频道,
如果调试频道打开太多, 日志会特别长, 动不动就几十G, 别说读日志, 就连用vim打开都特别慢, 这也不是办法.

怎么办呢? 这个问题大概可以从两个方面解决. 长期来说, 就是要在不断的调试中积累经验, 慢慢了解哪些bug适合观察哪些调试频道的日志.
而短期来说, 就是要尽可能"充分榨干"已有信息的利用价值.

回到上面提到的字符串参数: 005c45a0 L"\93c8\e100\
注意这里前面有一个十六进制数, 005c45a0, 我在前面的记述中故意忽略了, 实际上是因为我刚开始调试的时候自己确实忽略了这一线索.
这个十六进制数是什么意思呢? 通过阅读 GetTextExtentPoint32W接口的定义, 很容易确定这是一个指针. 同时,
这个函数还有一个参数用来指定字符串的长度. 函数的原型摘录如下:
BOOL WINAPI GetTextExtentPoint32W(
HDC hdc, /* [in] Handle of device context */
LPCWSTR str, /* [in] Address of text string */
INT count, /* [in] Number of characters in string */
LPSIZE size) /* [out] Address of structure for string size */

什么样的字符串变量需要配合另一个参数才能确定字符串的长度呢?
答案是, 这是一个指向不定长字符串的指针.
既然是一个指向不定长字符串的指针, 那么就应该会有一次内存分配的过程.
果然, 如果我们以这句GetTextExtentPoint32W日志为起点向上搜索"005c45a0"这个数字的话, 确实能找到分配内存的日志:
===
0024:Call ntdll.RtlAllocateHeap(005c0000,00000000,00000480) ret=7d1d0521
0024:Ret ntdll.RtlAllocateHeap() retval=005c45a0 ret=7d1d0521
===

紧接着可以看到类似这样的日志:
===
trace:richedit:ME_InsertRunAtCursor Shift length:239
trace:richedit:ME_PropagateCharOffset PropagateCharOffset(L"\r", 239)
===
看到richedit相关的日志, 即使我们对于richedit的api不熟悉, 至少也会强烈地怀疑这附近的代码与我们要研究的bug有很大的关系.

另一方面, 向上数十行, 又可以看到这样的日志:
===
0024:Call KERNEL32.MultiByteToWideChar(00000000,00000000,0033e32c
"\xef\xbb\xbf\xe5\x9f\x8e\xe5\xb8\x82\xe7\x83\xad\xe7\x82\xb9\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8\xe4\xb8\x8e\xe6\x9c\x80\xe7\xbb\x88\xe7\x94\xa8\xe6\x88\xb7\xe8\xae\xb8\xe5\x8f\xaf\xe5\x8d\x8f\xe8\xae\xae\r\n============================\r\n\r\n\xe8\xaf\xb7\xe8\xae\xa4\xe7\x9c\x9f\xe9\x98\x85"...,00001000,00339efa,00001000)
ret=7d1b0a13
===
这里 MultiByteToWideChar()是 Windows下用来转换编码的函数, 左右是把一个多字符字符串(MultiByte)
转换为 宽字符串, 也就是说, 把GBK编码, UTF8编码等的字符串转换为UTF16编码.

MultiByteToWideChar()的原型如下:
INT WINAPI MultiByteToWideChar( UINT page, DWORD flags, LPCSTR src, INT srclen,
LPWSTR dst, INT dstlen )
从日志可以看出, 这里page = 00000000, 通过google可以知道, page=0就是将ANSI
Codepage转换为UTF16, 在简体中文Windows系统上就是将GBK编码转换为UTF16. 那么, 转换的结果, 也就是
dst字符串, 究竟是不是我们见到的乱码字符串呢?
很遗憾从日志中没办法直接看出来.
不过, 这里有多种解决的方案, 其中一个方案就是, 添加新的调试频道, 重新运行一次, 生成更详细的日志.

我们到这里已经又找到一个可疑的函数, MultiByteToWideChar(), 从源代码中可以看到, 这个函数对应的调试频道是nls,
那么我们可以重新用下面的命令行运行drcom:
WINEDEBUG=+relay,+font,+richedit,+nls wine 校园网客户端.exe
这次生成的日志会更大, 但是当我们定位到日志的同一个地方的时候, 就会发现dst确实是 L"\93c8\e100..." 这样一个字符串!

到这里, 我们离成功又近了一步.

Qian Hong

unread,
Aug 23, 2012, 11:49:03 AM8/23/12
to gz...@googlegroups.com
回顾与补充;
- 上一节的目的是, 找出乱码字符串的来源. 思路是, 先把乱码字符串转换成十六进制表示, 然后从日志中搜索相应的十六进制数,
找到日志中的字符串之后, 进一步搜索字符类型指针的来源. 找到为字符类型指针分配内存的日志之后, 通过观察相邻的日志,
也就是所谓的"不同调试频道互相定位", 确定出该内存分配的语句是出现在richedit相关的函数中.
- 推荐一个网站, 可以将unicode字符转换成各种形式, 非常方便: http://www.rishida.net/tools/conversion/
- 上一节的推理过程存在一些跳跃, 最严重的地方就是,
没有解释MultiByteToWideChar输出的字符串跟我们当时搜索的指针是什么关系, 其实这是从log中无法直接发现的,
但是对照源代码可以得到严格的解释, 不过这样一来记述的篇幅会变得非常长, 所以略去. 简单地说, 就是苦力活.


接下来我们要研究的问题是, 既然MultiByteToWideChar()把某个原始字符串转换为乱码字符串, 那么这个原始字符串是否是正常的呢?
如果原始字符串也是乱码的, 那么我们又需要用类似的方法如法炮制, 继续往前追溯, 熟悉之后这也变成一份苦力活了.
如果原始字符串不是乱码, 那么我们需要考虑的问题就是, 为什么转换之后变成乱码了?

为了研究原始的字符串, 我们可以换一种形式把MultiByteToWideChar()函数的src参数打印出来
--- snip ---
/* src_str.c */
#include<stdio.h>
int main(void)
{
printf("\xef\xbb\xbf\xe5\x9f\x8e\xe5\xb8\x82\xe7\x83\xad\xe7\x82\xb9\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8\xe4\xb8\x8e\xe6\x9c\x80\xe7\xbb\x88\xe7\x94\xa8\xe6\x88\xb7\xe8\xae\xb8\xe5\x8f\xaf\xe5\x8d\x8f\xe8\xae\xae\r\n============================\r\n\r\n\xe8\xaf\xb7\xe8\xae\xa4\xe7\x9c\x9f\xe9\x98\x85");
return 0;
}
--- snip ---
编译之后运行, 猜猜输出是什么?
输出是
"城市热点有限公司与最终用户许可协议
============================

请认真阅"

于是, 我们很幸运地发现, src是一个有意义的字符串, 没有乱码.
既然如此, 我们剩下的问题就是, 为什么转换之后变成乱码了? 是MultiByteToWideChar()这个函数有bug,
还是别人传给MultiByteToWideChar()的某个参数是错的?
通过阅读MultiByteToWideChar()的源码, 可以发现这个函数是在kernel32.dll中实现的. 而我们早在第一节就发现,
从windows下复制一个riched20.dll, 就可以解决乱码的问题, 因此可以推测,
bug不在MultiByteToWideChar()函数上,
而是wine版本的riched20.dll中的某一行传给MultiByteToWideChar()的参数错了.

到这个时候, 如果不了解MultiByteToWideChar()这个函数, 就有必要好好恶补一下了:
http://msdn.microsoft.com/en-us/library/windows/desktop/dd319072(v=vs.85).aspx
由于MultiByteToWideChar()是一个用来转换编码的函数, 它可以把不同的编码转换为Unicode wide char,
而现在转换结果出错了, 我们需要分别了解src字符串是什么编码, 以及MultiByteToWideChar()当时用什么方式进行转换.

如果对Linux终端的编码有一定的了解, 就可以知道, 既然我们之前的src_str.c程序打印出来的字符在终端能正常显示,
而终端默认又是UTF8编码的, 那么我们打印的那段字符串肯定是UTF8编码的. 假如不了解这一点也没关系,
我们可以把src_str.c运行的结果重定向到一个文件中, 然后用file命令查看文件编码:
$ file src_str.txt
src_str.txt: UTF-8 Unicode (with BOM) text, with CRLF line terminators

接下来, 再一次认真的观察MultiByteToWideChar()接收到的参数:
0024:Call KERNEL32.MultiByteToWideChar(00000000,00000000,0033e32c
"\xe4\xb8\x8a\xef\xbc\x8c\xe6\x82\xa8\xe4\xb8\x8d\xe5\xbe\x97\xe5\xb0\xb1\xe8\xbd\xaf\xe4\xbb\xb6\xe8\xbf\x9b\xe8\xa1\x8c\xe8\xbf\x98\xe5\x8e\x9f\xe5\xb7\xa5\xe7\xa8\x8b\xef\xbc\x8c\xe5\x8f\x8d\xe7\xbc\x96\xe8\xaf\x91\xe6\x88\x96\xe5\x8f\x8d\xe6\xb1\x87\xe7\xbc\x96\xe3\x80\x82\r\n\r\n\r\n\xe6\x9c\xac\xe5"...,000006cf,00339efa,00001000)
ret=7d1b0a13

根据MSDN的接口说明, 在根据wine的头文件中对 CP_ACP, CP_UTF8等的定义 (可以用ctags帮助查找),
我们发现传给MultiByteToWideChar()的第一个参数居然是CP_ACP, 而同时传进去的字符串却是utf8编码的文本!

到这里, 我们已经找到根本原因了 ;-)
剩下来要做的事情, 就是如何识别输入的文本, 当输入为UTF8编码的时候,
传一个CP_UTF8给MultiByteToWideChar(), 而当输入为ANSI编码的时候,
就仍然传CP_ACP作为MultiByteToWideChar()的第一个参数.

Qian Hong

unread,
Aug 23, 2012, 12:13:59 PM8/23/12
to gz...@googlegroups.com
离成功只有一步之遥, 然而我差点死在这一步上.

先回顾一下日志:

trace:richedit:ME_StreamIn stream==0x33fd60 editor==0x5c28e8 format==0x1
trace:richedit:ME_StreamInText 00000001 0x33e320
0024:Call KERNEL32.MultiByteToWideChar(00000000,00000000,0033e32c
"\xef\xbb\xbf\xe5\x9f\x8e\xe5\xb8\x82\xe7\x83\xad\xe7\x82\xb9\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8\xe4\xb8\x8e\xe6\x9c\x80\xe7\xbb\x88\xe7\x94\xa8\xe6\x88\xb7\xe8\xae\xb8\xe5\x8f\xaf\xe5\x8d\x8f\xe8\xae\xae\r\n============================\r\n\r\n\xe8\xaf\xb7\xe8\xae\xa4\xe7\x9c\x9f\xe9\x98\x85"...,00001000,00339efa,00001000)
ret=7d1b0a13
0024:Ret KERNEL32.MultiByteToWideChar() retval=0000087e ret=7d1b0a13

从日志中可以看出, ME_StreamInText() 这个函数跟 MultiByteToWideChar() 这个函数是紧挨着的,
可以猜测可能是前者调用了后者.

从源代码中, 可以在ME_StreamInText()函数内部看到这么一段代码, 证实我们的猜测是正确的:
if (!(dwFormat & SF_UNICODE))
{
/* FIXME? this is doomed to fail on true MBCS like UTF-8,
luckily they're unlikely to be used as CP_ACP */
nWideChars = MultiByteToWideChar(CP_ACP, 0, stream->buffer,
stream->dwSize, wszText, STREAMIN_BUFFER_SIZE);
pText = wszText;
}
else
{
nWideChars = stream->dwSize >> 1;
pText = (WCHAR *)stream->buffer;
}

这段代码先判断dwFormat这个flag是否蕴含SF_UNICODE, 如果是, 就无须转换, 如果否, 就把输入的字符串当作CP_ACP编码进行转换.
从代码的注释中也可以发现很有意思的事情, 原来Wine的开发者也注意到这里可能会有bug, 结果就是真的有bug ;-)

那么, 如何正确地修复这个bug呢? 我一开始也纳闷为什么wine的开发者明明已经考虑到存在由UTF8引起的bug的可能性了,
却没有在代码中实现对UTF8的支持. 可是, 认真的读完这个函数接口的所有参数定义之后得出一个要命的结论: 尼玛这个bug根本没办法修啊!

为什么这么说呢? 我们从代码中可以看到, 这个函数接口通过dwFormat这个flag来识别编码, 可是读遍MSDN对这个flag的说明,
会发现压根没有提到UTF8! 只有SF_UNICODE或者非SF_UNICODE! 尼玛能不这么坑爹吗!
http://msdn.microsoft.com/en-us/library/windows/desktop/bb774302(v=vs.85).aspx

那么, 我们可以简单地把上述代码中第一个条件分支的编码改为UTF8吗? 当然不可以, 这样就会导致修复一个bug的同时引起另一个bug.

到这里, 我无限绝望无限悲哀地想起Wine开发者说过一句话: Wine中容易修的bug都被人修了...

当然, 后来我幸运地找到了这个bug的解决方法, 不然也就不会有这篇分享文章了.
其实, 答案就隐藏在前文中...

Qian Hong

unread,
Aug 23, 2012, 12:41:10 PM8/23/12
to gz...@googlegroups.com
回顾一下, 前文用过哪些方法判断src字符串的编码?
重点看一下前文的第二种方法, 用file来检测:

$ file src_str.txt
src_str.txt: UTF-8 Unicode (with BOM) text, with CRLF line terminators

仔细看, 看到什么不对头的没有?
没看到, 再看一遍?
BOM有木有!!! BOM有木有!!!
就是这个坑爹的BOM, 多少程序员曾经栽在这个坑上!!!

简单介绍一下什么是BOM:
http://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F
为了识别大端字节序还是小端字节序, 通过将某些记号附在文本的前头, 这就是 "字节顺序标记"(英语:byte-order mark,BOM)

在类Unix系统中, 对于UTF8文本, 通常不建议添加BOM, 因为会影响脚本解释器识别shebang,
而Windows程序则比较多地在UTF8文本前加入BOM.
究竟在UTF8文本中加入BOM好还是不好, 这个暂且不争论, 估计也难有定论, 不过, 无数的程序员曾经因为BOM引起的诡异问题而抓狂,
这确实是惨痛的实事. 让人稍感安慰的是, 几乎每个程序员在亲自吃过BOM的亏之前, 都难以获得对BOM的面议 XD
所以, 哪怕今天哪位朋友读了这段文字, 只要他还没亲自吃过BOM的亏, 我相信一定有机会的 =)
这么想, 可以让我在臆想的幸灾乐祸中获得一些心理平衡...

好, 回到正题, UTF8的BOM是以下三个字节:
EF BB BF

我们重新看一下日志:
===


0024:Call KERNEL32.MultiByteToWideChar(00000000,00000000,0033e32c
"\xef\xbb\xbf\xe5\x9f\x8e\xe5\xb8\x82\xe7\x83\xad\xe7\x82\xb9\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8\xe4\xb8\x8e\xe6\x9c\x80\xe7\xbb\x88\xe7\x94\xa8\xe6\x88\xb7\xe8\xae\xb8\xe5\x8f\xaf\xe5\x8d\x8f\xe8\xae\xae\r\n============================\r\n\r\n\xe8\xaf\xb7\xe8\xae\xa4\xe7\x9c\x9f\xe9\x98\x85"...,00001000,00339efa,00001000)
ret=7d1b0a13

===

天哪, BOM就在眼前, 可是多少人视而不见? 前三个字节, 正好是:
"\xef\xbb\xbf"

到这里, 谜题终于揭晓, 这个bug的修复方式也变得显然了, 就是先检测一下文本有没有BOM, 有的话就按UTF8的方式处理, 没有的话就按ANSI的方式处理.

就是这么简单! 我终于推翻了Wine开发者的魔咒, 找到了一个简单但是还没有人修的bug!

Qian Hong

unread,
Aug 23, 2012, 12:57:05 PM8/23/12
to gz...@googlegroups.com
在经历了曲折的调试分析之后, 最后的修bug打patch阶段反而显得波澜不惊.
按照Wine开发的一些规范流程, 需要写测试程序验证你的想法是正确的, 将测试程序集成到wine的测试框架中, 然后发送测试用例和修bug的补丁,
等待补丁在wine的自动测试机器上自动编译和测试, 如果测试通过了, 再看看开发者有没有对你的补丁提出什么改进意见, 如果有的话,
按照前辈的反馈好好改一改, 反复改到补丁被项目接收.

这个bug的测试用例在这里:
http://source.winehq.org/git/wine.git/commit/01ebba0fc4c28c3a64a8daa96a18755233057dfe
修复bug的补丁在这里:
http://source.winehq.org/git/wine.git/commit/cb8c500ee67a64c4e5f08434d5ae0634bf82e375

对于有兴趣参与wine开发的同学则需要注意, 到最后阶段反而更需要很多耐心, Wine项目对补丁的要求比较苛刻, 尤其对于新人更是如此.
一位Wine开发者告诉过我, 有时候让补丁被wine项目接受甚至比发现和修复bug本身还难.

这两个补丁被wine接受时候, 随着Wine 1.5.11的发布, 这个乱码的问题也就不存在了.
而下面要说的, 就是, 其实, 这篇文章, 是一篇, 赤裸裸的炫耀帖!!!

我终于在Wine的release note里看到了我的名字!!! 我已等待了一年!!!
http://www.winehq.org/announce/1.5.11


[注] 为避免我的补丁中的缺陷给别人造成不好的示范, 这里需要自爆一下, 我的补丁其实是有bug的, 而这个bug在第二天被另一个开发者修复:
http://source.winehq.org/git/wine.git/commit/3a2de10a9ccdb6a93930aa76e582d6bc397d0347

Qian Hong

unread,
Aug 23, 2012, 1:17:31 PM8/23/12
to gz...@googlegroups.com
最后, 对这篇文章稍微做一点总结.

- winetricks对于wine的调试有很大的帮助. 如果某个bug被证实可以通过copy某个Windows Dll来解决,
那么就已经能把bug的范围限定在一个小模块里面了.
因此, 如果哪位朋友遇到bug, 请尽量去报一下; 如果哪位朋友知道某个bug的workaround方法, 为了大家的幸福, 请一定要去报!
因为这样, 世界上某个角落的某个Wine开发者, 可能会因为你提供的信息, 节省了好几个小时甚至好几天的时间!

- 当winetricks本身有bug的时候, 也应该给winetricks报bug, 因为调试的过程最讨厌的就是碰上bug之外的bug, 经常会让人抓狂.

- 报bug的过程其实可以学到很多排查诊断和开发调试的知识, 前提是你报过的bug或读过的bug足够多.
当我报了一定数量的bug之后, 我就开始积累了一些看起来比较容易修复的bug, 而我也是从这些容易修复的bug入门开始wine的开发的.
Wine项目有一个开发者, 以bug report的形式记录了他对Wine的一千多个bug的分析思路,
他的目的是为新开发者提供调试分析的案例, 曾经帮助很多开发者入门.
更多例子参见: http://wiki.winehq.org/DeveloperExamples

- 如果有哪位朋友对这篇文章的细节有疑问, 欢迎提出来讨论 ;-)
如果这篇文章能够让大家读后留下"Wine开发还是挺好玩的"这样的印象, 那我的目标就已经实现了;
万一如果有同学受到这篇文章的影响而稍微有点兴趣参与wine开发, 那实在是我的荣幸, 欢迎多多交流!

稍微延伸一下:

- 虽然不是每个人都对Wine感兴趣, 但是我相信LUG的大部分人都对技术感兴趣. 借这篇文章抛砖引玉,
也希望更多朋友分享一下自己在开发调试中的心得体会. 我个人特别期待有人愿意去hack libreoffice,
然后分享一下如何解决MS文档格式兼容的问题. 可惜我没有三头六臂, 不然我一定会去做的 ;-)

- 分享技术心得, 真的是利己利人的事情, 你花越多的时间整理和分享, 实际上自己收获越多.
刘未鹏写过一篇文章, 叫做 "知其所以然: 为什么算法那么难"
http://mindhacks.cn/2011/07/10/the-importance-of-knowing-why-part3/
这篇文章给我很多启发, 让我受益匪浅, 在这里也推荐给没读过的朋友.
类似的, 我们也可以提出一个问题: "为什么调试那么难?"
而经常对自己的调试过程进行整理总结并分享出来, 我觉得就是对自己一种很好的锻炼, 可以帮助自己发展出一套适合自己的调试技巧,
可以向"知其所以然"的目标不断靠近.

^_^ (完)

Face2Wind

unread,
Aug 23, 2012, 9:43:43 PM8/23/12
to gz...@googlegroups.com
如此好帖,不知Qian Hong有无打算整理一下作为博客类似的方式跟更多人分享呢?

虽然里面很多技术性的内容都看不懂,但Qian Hong为了这个BUG做的付出确是赤裸裸展现在字里行间,让我不能自已。平时我们使用wine的时候,或许没啥感觉,甚至经常觉得这里有问题那里有问题很不爽,wine表现在很多方面显得不如人意。但是,仔细想想,如此庞大的wine,得需要多少和Qian Hong这样甚至更辛苦为wine付出的人啊?

是的,就算你不说,我也知道这是炫耀贴,但是值得炫耀,也值得分享,让更多的人知道这是一份多么需要勇气、耐心以及细心的工作。

Qian Hong

unread,
Aug 23, 2012, 11:13:05 PM8/23/12
to gz...@googlegroups.com
2012/8/24 Face2Wind <dream.li...@gmail.com>:
> 如此好帖,不知Qian Hong有无打算整理一下作为博客类似的方式跟更多人分享呢?
>
谢谢Face2Wind鼓励 ;-)

我有计划整理之后重新发布, 目前的想法是先把初稿第一时间发在gzlug跟兄弟们分享, 然后看看反馈如何, 如果能够收集到足够多的反馈意见,
我可以对如何改进有个把握, 等过一定时间之后再整理发布. 不过目前还没有想好要发布在什么地方. 最近几个月积累了一批同类的素材,
计划写文章分享, 如果有第三季第四季, 也同样会第一时间发在gzlug给大伙过目, 然后再根据反馈考虑是否要进一步改进整理重新发布 ;-)

> 虽然里面很多技术性的内容都看不懂,但Qian Hong为了这个BUG做的付出确是赤裸裸展现在字里行间,让我不能自已。平时我们使用wine的时候,
> 或许没啥感觉,甚至经常觉得这里有问题那里有问题很不爽,wine表现在很多方面显得不如人意。但是,仔细想想,如此庞大的wine,得需要多少和
> Qian Hong这样甚至更辛苦为wine付出的人啊?
>

按照Codeweavers公司的统计, 一个有经验的Wine开发者, 修复一个Wine的bug, 平均需要10个小时, 但是我因为是新手,
花的时间更多, 这篇文章中的bug前后大概花了我一个星期的工作日的时间, 或者说大概40小时.
Wine项目的bugzilla目前总共有30000多个bug, 其中出去重复的bug, 无效的bug, 放弃的bug等等,
大概有10000多个bug是有效并且被修复了的, 从这个数据可以对Wine项目开发者投入的时间有个感觉.
注意这里的时间仅仅考虑了修复bug的时间, 并没有考虑从零开始实现一个新api的时间, 这部分时间就无法估量了.

还有另一个角度可以感受一下Wine项目的工作量, 就是ohloh.net的统计数据:
https://www.ohloh.net/p/wine
ohloh.net是世界上最齐全的开源代码索引数据库, 提供了丰富的统计分析比较的功能.

> 是的,就算你不说,我也知道这是炫耀贴,但是值得炫耀,也值得分享,让更多的人知道这是一份多么需要勇气、耐心以及细心的工作。

--

Face2Wind

unread,
Aug 24, 2012, 12:05:07 AM8/24/12
to gz...@googlegroups.com
确实有必要整理重新发布的,最起码我是挺期待的。相信gzlug里很多朋友也期待!不过需要整理得条理清晰点,让一些懂得不是很多甚至基本不懂这方面技术的人能容易理清文章思路,看到重点。
我有计划整理之后重新发布, 目前的想法是先把初稿第一时间发在gzlug跟兄弟们分享, 然后看看反馈如何, 如果能够收集到足够多的反馈意见,
 

这个要有,呵呵!
如果有第三季第四季, 也同样会第一时间发在gzlug给大伙过目, 然后再根据反馈考虑是否要进一步改进整理重新发布 ;-)
 

确实感受到了,之前只是大概估算了一下,解决一个bug尚且如此折腾和跟踪调试,其他成千上万的Bug更不必说。而且bug的筛选也是很大的工作量~~
btw,https://www.ohloh.net/里面的统计确实很不错。
按照Codeweavers公司的统计, 一个有经验的Wine开发者, 修复一个Wine的bug, 平均需要10个小时, 但是我因为是新手,
花的时间更多, 这篇文章中的bug前后大概花了我一个星期的工作日的时间, 或者说大概40小时.
Wine项目的bugzilla目前总共有30000多个bug, 其中出去重复的bug, 无效的bug, 放弃的bug等等,
大概有10000多个bug是有效并且被修复了的, 从这个数据可以对Wine项目开发者投入的时间有个感觉.
注意这里的时间仅仅考虑了修复bug的时间, 并没有考虑从零开始实现一个新api的时间, 这部分时间就无法估量了.

还有另一个角度可以感受一下Wine项目的工作量, 就是ohloh.net的统计数据:
https://www.ohloh.net/p/wine
ohloh.net是世界上最齐全的开源代码索引数据库, 提供了丰富的统计分析比较的功能.
--
Regards,
Qian Hong

-
Sent from Ubuntu
http://www.ubuntu.com/


--
人脑是我已知的最强的操作系统,装软件要小心!

Qian Hong

unread,
Aug 24, 2012, 12:10:12 AM8/24/12
to gz...@googlegroups.com
2012/8/24 Face2Wind <dream.li...@gmail.com>:

> 确实有必要整理重新发布的,最起码我是挺期待的。相信gzlug里很多朋友也期待!不过需要整理得条理清晰点,让一些懂得不是很多甚至基本不懂这方面技术的人能容易理清文章思路,看到重点。

恩, 如何整理对我来说是一个难点, 因为我无法从读者的角度发现文章中不清晰的地方, 希望感兴趣的朋友多提提意见, 哪些地方写得不够清晰,
有什么疑问尽管提, 我会在这里尽量解答技术细节, 也只有收集到足够的信息, 我才知道接下来如何修改.

我专门挑在周末之前写完这篇文章, 就是希望大伙周末有空可以帮忙提提意见 ;-)

Ma Xiaojun

unread,
Aug 24, 2012, 12:33:49 AM8/24/12
to gz...@googlegroups.com
Qian Hong,你必須開一個博客了!
像這樣的“連載”,只能線性回覆,發出不能修改的情況下,如何評論和完善?
當然我對你的工作和分享表示感謝!

Qian Hong

unread,
Aug 24, 2012, 1:20:24 AM8/24/12
to gz...@googlegroups.com
2012/8/24 Ma Xiaojun <damag...@gmail.com>:
> Qian Hong,你必須開一個博客了!
谢谢建议!
首先发布在gzlug的原因是, 希望可以多发起一些技术讨论.
而邮件列表的讨论质量一般比较高, 如果维护自己的博客的话, 我对于"飘过"之类的评论会比较无奈和反感.
如果先发布在邮件列表里经过一轮讨论, 然后再整理完善发布到某个群体技术博客上, 最后再整理出一个最终版发布在wine的官方wiki上,
这样的做法会不会好一点? 正好我接下来准备开始翻译wine的一些官方文档, 回馈给wine社区.

> 像這樣的“連載”,只能線性回覆,發出不能修改的情況下,如何評論和完善?

不能修改确实是一个硬伤, 不过我还是希望像我们这种引用回复的方式可以组织出条理清晰的讨论.
邮件列表这种讨论形式在国外技术社区中还是很高效的, 希望有更多朋友喜欢.
另外, 建议大家回复的时候尽量裁剪引文, 只引用关键的原文, 并且把回复放在因为的下方, 这样线索就会比较清晰易读.

> 當然我對你的工作和分享表示感謝!
也谢谢你对ibus的贡献, 期待合适的时候你也分享一下参与ibus贡献的心得, 或者你在其他项目上的心得 ;-)
加油~

Qian Hong

unread,
Aug 24, 2012, 1:22:05 AM8/24/12
to gz...@googlegroups.com
2012/8/24 Qian Hong <frac...@gmail.com>:

> 另外, 建议大家回复的时候尽量裁剪引文, 只引用关键的原文, 并且把回复放在因为的下方, 这样线索就会比较清晰易读.

错别字, 抱歉. 应该是, "把回复放在引文的下方"
我这封回复就是 "裁剪引文" 和 "回复放在引文下方" 的一个例子.

Jactry

unread,
Aug 24, 2012, 11:13:06 PM8/24/12
to gz...@googlegroups.com
在 2012年8月24日 下午1:20,Qian Hong <frac...@gmail.com>写道:
2012/8/24 Ma Xiaojun <damag...@gmail.com>:
> Qian Hong,你必須開一個博客了!
谢谢建议!
首先发布在gzlug的原因是, 希望可以多发起一些技术讨论.
而邮件列表的讨论质量一般比较高, 如果维护自己的博客的话, 我对于"飘过"之类的评论会比较无奈和反感.
如果先发布在邮件列表里经过一轮讨论, 然后再整理完善发布到某个群体技术博客上, 最后再整理出一个最终版发布在wine的官方wiki上,
这样的做法会不会好一点? 正好我接下来准备开始翻译wine的一些官方文档, 回馈给wine社区.
或許可以用github,不僅可以有前後的版本對照,也有issues可以討論~

Qian Hong

unread,
Aug 25, 2012, 2:20:35 AM8/25/12
to gz...@googlegroups.com
2012/8/25 Jactry <jact...@gmail.com>:
> 或許可以用github,不僅可以有前後的版本對照,也有issues可以討論~
这个办法看起来不错!

Qingping Hou

unread,
Sep 1, 2012, 7:23:56 PM9/1/12
to gz...@googlegroups.com
真心好厉害。。。

洪同学,你的blog是时候该搭起来了,哈哈

2012/8/25 Qian Hong <frac...@gmail.com>:

> --
> 您收到此邮件是因为您订阅了 Google 网上论坛的“广州 GNU/Linux 用户组”论坛。
> 要向此网上论坛发帖,请发送电子邮件至 gz...@googlegroups.com
> 要取消订阅此网上论坛,请发送电子邮件至 gzlug+un...@googlegroups.com
> 若有更多问题,请通过 http://groups.google.com/group/gzlug?hl=zh-CN 访问此网上论坛。
>

老谦

unread,
Sep 1, 2012, 10:55:32 PM9/1/12
to gz...@googlegroups.com

我也觉得哈哈

JunLe Li

unread,
Sep 2, 2012, 2:00:58 AM9/2/12
to gz...@googlegroups.com
2012/8/25 Qian Hong <frac...@gmail.com>

2012/8/25 Jactry <jact...@gmail.com>:
> 或許可以用github,不僅可以有前後的版本對照,也有issues可以討論~
这个办法看起来不错!


Jekyll + Disqus 也不错 XD

Joseph Pan

unread,
Sep 2, 2012, 2:02:26 AM9/2/12
to gz...@googlegroups.com
在 2012年9月2日 下午2:00,JunLe Li <lij...@gmail.com> 写道:
> Jekyll + Disqus 也不错 XD

同感

--
Yours respectfully,

潘伟洲(Joseph Pan)

josephpan.net

Liutos

unread,
Sep 2, 2012, 2:05:09 AM9/2/12
to gz...@googlegroups.com
自己写博客程序搭在GitHub Pages上也可以嘛~

--
您收到此邮件是因为您订阅了 Google 网上论坛的“广州 GNU/Linux 用户组”论坛。
要向此网上论坛发帖,请发送电子邮件至 gz...@googlegroups.com
要取消订阅此网上论坛,请发送电子邮件至 gzlug+un...@googlegroups.com
若有更多问题,请通过 http://groups.google.com/group/gzlug?hl=zh-CN 访问此网上论坛。




--
Liutos Love Linux LaTeX Lisp Ling

我的GitHub主页:https://github.com/Liutos

JunLe Li

unread,
Sep 2, 2012, 2:26:02 AM9/2/12
to gz...@googlegroups.com
这个也要基于Jekyll吧,难道楼上想用js做伪动态的blog?

(其实我就在Bitbucket,做了一个js伪动态的blog XDDDD)

--

LiJ...@gmail.com | Public Key | Twitter | SinaWeibo



2012/9/2 Liutos <mat.l...@gmail.com>

Joseph Pan

unread,
Sep 2, 2012, 2:26:45 AM9/2/12
to gz...@googlegroups.com
在 2012年9月2日 下午2:05,Liutos <mat.l...@gmail.com> 写道:
> 自己写博客程序搭在GitHub Pages上也可以嘛~

可以 Jekyll + Disqus + Github Pages 。

在 2012年8月25日 上午11:13,Jactry <jact...@gmail.com> 写道:
> 或許可以用github,不僅可以有前後的版本對照,也有issues可以討論~

用 issues 来做留言比较适合,做评论的话好像还不能引用到具体某一篇文章。

JunLe Li

unread,
Sep 2, 2012, 2:32:01 AM9/2/12
to gz...@googlegroups.com
2012/9/2 Joseph Pan <cs.w...@gmail.com>


在 2012年8月25日 上午11:13,Jactry <jact...@gmail.com> 写道:
> 或許可以用github,不僅可以有前後的版本對照,也有issues可以討論~

用 issues 来做留言比较适合,做评论的话好像还不能引用到具体某一篇文章。

这个可以啊。disqus根据identitier和url来决定页面显示的comments。(不同的文章,显示不同的comments)
 

Joseph Pan

unread,
Sep 2, 2012, 2:41:43 AM9/2/12
to gz...@googlegroups.com
在 2012年9月2日 下午2:32,JunLe Li <lij...@gmail.com> 写道:
> 这个可以啊。disqus根据identitier和url来决定页面显示的comments。(不同的文章,显示不同的comments)

评论者如果在提交issue的时候没有指定是在评论哪篇文章,这样洪兄都不知道评论的是哪一篇,何况disqus?久而久之,issues上面也将会有很多和Spam无异的评论。

即使可以通过添加额外字段(identifier or
url)指定是针对哪篇文章,那恐怕也会对评论者造成了不必要的麻烦。我想我不会为了comment一篇文章还要遵循某某规范,copy某个url。这是无趣的工作。

Liutos

unread,
Sep 2, 2012, 4:31:19 AM9/2/12
to gz...@googlegroups.com
我这里安装不了Jekyll,所以我只是自己写了一套工具来帮我生成文章页面和首页而已,真的是静态的,因为我的JS功力很烂,不太懂。

Robin Lee

unread,
Sep 2, 2012, 4:37:59 AM9/2/12
to gz...@googlegroups.com
就这样歪楼了~

2012/9/2 Liutos <mat.l...@gmail.com>

JunLe Li

unread,
Sep 2, 2012, 7:44:44 AM9/2/12
to gz...@googlegroups.com
2012/9/2 Joseph Pan <cs.w...@gmail.com>

在 2012年9月2日 下午2:32,JunLe Li <lij...@gmail.com> 写道:
> 这个可以啊。
disqus根据identitier和url来决定页面显示的comments。(不同的文章,显示不同的comments)

评论者如果在提交issue的时候没有指定是在评论哪篇文章,这样洪兄都不知道评论的是哪一篇,何况disqus?久而久之,issues上面也将会有很多和Spam无异的评论。

即使可以通过添加额外字段(identifier or
url)指定是针对哪篇文章,那恐怕也会对评论者造成了不必要的麻烦。我想我不会为了comment一篇文章还要遵循某某规范,copy某个url。这是无趣的工作。

这个工作不是用户做的,是开发者之前设定好。disqus加载到页面的时候,自动根据Identifier加载thread。
用户做的,就是看到一个输入框,输入想评论的文字,然后Post。Identifier和Thread都不会乱的。(当然,前提是disqus配置好了。)
而blogger做的,就是在第一次配置好disqus。然后用markdown/HTML写博文,然后git到github。

2012/9/2 Liutos <mat.l...@gmail.com>

我这里安装不了Jekyll,所以我只是自己写了一套工具来帮我生成文章页面和首页而已,真的是静态的,因为我的JS功力很烂,不太懂。

Jekyll和js没有半毛钱关系。最近阮一峰的那篇博文介绍了Github+Jekyll的使用,其实很简单的。

2012/9/2 Robin Lee <robinl...@gmail.com>
就这样歪楼了~

XDDDDD.....

Joseph Pan

unread,
Sep 2, 2012, 8:19:38 AM9/2/12
to gz...@googlegroups.com
在 2012年9月2日 下午7:44,JunLe Li <lij...@gmail.com> 写道:
> 这个工作不是用户做的,是开发者之前设定好。disqus加载到页面的时候,自动根据Identifier加载thread。
> 用户做的,就是看到一个输入框,输入想评论的文字,然后Post。Identifier和Thread都不会乱的。(当然,前提是disqus配置好了。)
> 而blogger做的,就是在第一次配置好disqus。然后用markdown/HTML写博文,然后git到github。

还是不理解Github里的issue如何和disqus合作,还是我们两个聊得不是同个东西。

“Talk is cheap. Show me the code.",师弟可以发个示范网站的link给我看看,这样我好理解一点。;-)

JunLe Li

unread,
Sep 2, 2012, 9:17:10 AM9/2/12
to gz...@googlegroups.com

→_→ 原来聊的不是同样的东西。我的意思并不是issue的数据和disqus互通。只是disqis的评论系统足够好了,就不用issus了。
不过对于issue和disqus数据互通的想法,我觉得可以尝试一下。

On Sep 2, 2012 8:19 PM, "Joseph Pan" <cs.w...@gmail.com> wrote:

在 2012年9月2日 下午7:44,JunLe Li <lij...@gmail.com> 写道:

> 这个工作不是用户做的,是开发者之前设定好。disqus加载到页面的时候,自动根据Identifier加载thread。
> 用户做的,就是看到一个输入框,输入想评论的文字,然后Post。Ident...

还是不理解Github里的issue如何和disqus合作,还是我们两个聊得不是同个东西。

“Talk is cheap. Show me the code.",师弟可以发个示范网站的link给我看看,这样我好理解一点。;-)


--
Yours respectfully,

潘伟洲(Joseph Pan)

josephpan.net

--
您收到此邮件是因为您订阅了 Google 网上论坛的“广州 GNU/Linu...

Liutos

unread,
Sep 2, 2012, 10:32:00 AM9/2/12
to gz...@googlegroups.com
在 2012年9月2日 下午7:44,JunLe Li <lij...@gmail.com>写道:
我知道没关系,我的话唯一的重点是Jekyll装不上T_T

2012/9/2 Robin Lee <robinl...@gmail.com>
就这样歪楼了~

XDDDDD.....

--
您收到此邮件是因为您订阅了 Google 网上论坛的“广州 GNU/Linux 用户组”论坛。
要向此网上论坛发帖,请发送电子邮件至 gz...@googlegroups.com
要取消订阅此网上论坛,请发送电子邮件至 gzlug+un...@googlegroups.com
若有更多问题,请通过 http://groups.google.com/group/gzlug?hl=zh-CN 访问此网上论坛。

hiphen lee

unread,
Sep 7, 2012, 2:17:31 PM9/7/12
to gz...@googlegroups.com
当一show all,然后我就震惊了,赶紧猛击后退键,眼不见为净。。


--
Hiphen

larmbr zhan

unread,
Sep 11, 2012, 6:02:44 AM9/11/12
to gz...@googlegroups.com

2012/8/25 Qian Hong <frac...@gmail.com>
 >>2012/8/25 Jactry <jact...@gmail.com>:
 >> 或許可以用github,不僅可以有前後的版本對照,也有issues可以討論~
 >这个办法看起来不错!
现在才看到这帖,mark回去看。(偷懒了,那个bug还放着,罪过-.-)

P.S. 这种回帖方法, 排得好累~
Reply all
Reply to author
Forward
0 new messages