查看原文
其他

wine调试日志实现和实践

输出日志是一种很常见的调试方法,特别是诡异的多线程bug,随机崩溃等问题,在正确的地方输出日志缩小范围是最有效的方法。


其次对不熟悉的代码勤快打日志,可以很快熟悉代码的流程和作用;对于一个大型项目来说,一般都会先实现一套自己的日志函数:一是方便开发人员轻松打日志,快速debug;二可以统一日志格式和存储,方便用户报告日志,所以wine也不例外,而且wine的实现非常简洁。下面就详细总结一下。

调试通道 (debug channel)

wine定义了调试通道的概念来分类日志,将日志的记录和实际的输出分离,无需重新编译,就能灵活控制日志输出。

  • 每个调试通道有一个唯一的名字,长度不超过14个有效字符,一般一个模块至少定义了一个调试通道,比如 gdi32.dll模块,有一个名称叫gdi的调试通道。

  • 复杂的模块,为了细分日志定义了多个调试通道,比如gdi32.dll模块,还有定义clipping,region,font,bitmap,print,bitblt等调试通道。

  • 调试通道在代码里面来看实际是一个__wine_debug_channel 的结构体,刚好16个字节,非常符合 unix哲学的简单原则:


  • 日志一次只发送给一个调试通道。

  • 代码里面增加一个新的调试通道:

1) 包含 include/wine/debug.h

2) 然后用

WINE_DEFAULT_DEBUG_CHANNEL或

WINE_DEFAULT_DEBUG_CHANNEL宏来声明

  • 要知道一个模块定义了哪些调试通道,只需这样搜索该模块的所有源码:git grep _DEBUG_CHANNEL

向调试通道发送日志

wine把日志分成了4个级别,从高到低依次是:fixme/err(or)/warn/trace,对应的提供了4个宏来输出不同级别的日志到调试通道:FIXME/WARN/ERR/TRACE, 用起来非常清晰,简单;



最终都是调用函数 wine_dbg_log 来打日志:



其中的funcs.dbg_vlog 初始化时候指向 default_dbg_vlog



在wine线程创建成功后会 funcs.dbg_vlog 指向 ntdll/debugtools.c的


运行前开启调试通道

这样的格式定义环境变量:WINEDEBUG=[class][+/-]channel[,[class2][+/-]channel2]

其中:

* class是fixme,err,warn,trace这4个日志级别的一个单词,如果没有指定就开关所有的日志级别。

* channel就是要开关的调试通道的名称,all代表所有的通道。

* +就是开启指定调试通道的对应的日志级别。

* -就是关闭指定调试通道的对应的日志级别。


举例:

WINEDEBUG=warn+allWINEDEBUG=warn+dll,+heapWINEDEBUG=fixme-all,warn+cursor,+relay


如果没有定义WINEDEBUG环境变量,发给每个调试通道的fixme和err级别的日志都会输出;wine默认同时开启运行的调试通道是256个,由这个宏 MAX_DEBUG_OPTIONS 决定。


关键代码如下:



仅标记作用的调试通道

pid: 在每个日志的前面插入当前进程的ID号,格式: %04x


tid: 在每个日志的前面插入当前线程的ID号,格式: %04x

 

timestamp: 在每个日志的前面插入时间戳,相对系统启动的时间,单位秒,保留3位小数


特殊的高级的调试通道

seh: 记录所有的异常情况,快速定位程序崩溃地址。



relay: 无需修改代码,记录程序调用wine实现的所有API的详细参数和返回值,



snoop: 无需修改代码,记录程序对第三方native模块的所有导出函数的调用参数和返回值。


snoop是自己检查stack数据和反汇编来探测函数调用约定,参数和返回地址的,如果探测错了会影响程序的稳定,甚至导致程序崩溃,非常规情况下使用。



relay和snoop的缺点是记录的日志巨大导致程序反应非常慢,只能在没有任何思路,一筹莫展时使用。

运行中开关调试通道

方法1,

运行任务管理器(wine taskmgr),打开“进程”标签页,右键选中进程,在右键菜单里面选中“编辑调试频道”。 这个方法只能开关事先在WINEDEBUG环境变量里面列出的调试通道。

 

方法2, 

winedbg里面attach指定的wine进程,然后用set命令:


  • set + channel 开启指定通道的所有fixme/err/warn/trace日志

  • set – channel 关闭指定通道的所有fixme/err/warn/trace日志

  • set class + channel 开启指定通道的fixme/err/warn/trace日志的一类 

  • set class – channel 关闭指定通道的fixme/err/warn/trace日志的一类


winedbg的set命令也只能设置用WINEDEBUG已经开启了的调试通道。如果没有在WINEDEBUG里面定义的,就会提示: Unable to find debug channel xxx


方法3,

winedbg的attach指定的wine进程,手动修改debug_options和nb_debug_options的数据:因为debug_options是按照调试通道名称字符串比较排序的,所以开启多个通道需要手动排序。


这个方法适合运行程序时忘记设置WINEDEBUG,但是想查看某个调试通道日志的时候又不想重新运行程序的时候使用。



方法4,

一般正式发布的libwine.so没有调试符号,就只能反汇编定位debug_options和nb_debug_options的地址。


1) 先查询_wine_dbg_get_channel_flags的偏移, readelf -s libwine.so.1.0 | grep _wine_dbg :



2) 再查询libwine.so的基地址得到_wine_dbg_get_channel_flags的地址



3) 接着看_wine_dbg_get_channel_flags反汇编:



gcc习惯通过ebx寄存器来引用全局变量,

所以nb_debug_options的地址是:0xf75d303b+0x19dfc5+0x134;

debug_options的地址是:  0xf75d303b+0x19dfc5+0x260;

然后参考方法2的set命令修改内存即可。

relay 实现原理

在 LoadLibrary 内部检查开启了relay通道并且是加载wine内建的dll, 就调用RELAY_SetupDLL解析dll的导出函数表(IMAGE_DIRECTORY_ENTRY_EXPORT),对导出表的AddressOfFunctions数组每个成员,先备份原始值,然后修改成可以跳到relay_call函数的hack函数地址, hack函数在faked PE模块里面, 固定的24个字节大小, 形式都是这样的:



不同的API,变化的是里面的数字常量而已。在GetProcAddress 内部检查是否开启了relay通道,是就调用RELAY_GetProcAddress 返回 hack的函数地址.以user32模块的GetMenu举例,返回的hack的GetMenu 函数:



原始的GetMenu地址:



relay_call里面调用 relay_trace_entry/relay_trace_exit来记录函数的进和出,以及调用真实的API。


snoop的实现原理

在 LoadLibrary 内部检查开启了snoop通道并且是加载native的dll,就调用 SNOOP_SetupDLL,解析dll的导出函数,对每个导出函数动态分配一块可读写和执行的hack内存。


在 GetProcAddress 内部检查开启了snoop通道,就调用 SNOOP_GetProcAddress,对hack内存填充一个可以跳到 SNOOP_Entry 函数的jmp指令, 然后返回把这个hack内存块的首地址。


SNOOP_Entry探测函数的返回地址/调用约定/调用参数,打印出来,然后把当前EIP设置成真实的导出函数,把返回地址设置SNOOP_Return。


用日志来解决实际的问题

先收集日志,然后重点看err:,fixme:,seh: 的日志,一般就是出问题的地方了。


Deepin


【相关链接】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存