使用PCI IDE Controller读写硬盘 - 0

前言

依稀记得我一年半以前曾经写过一篇关于PIO读硬盘数据的文章,当时就提到了读写硬盘操作很复杂,完全可以拿来做一个系列来写。当时也是真有写出一个系列的冲动,不过不巧的是,由于那段时间换工作,新的工作和底层的关系不太大,也就没时间继续读相关文档来把这个系列写下来。我现在还很清楚的记得当时有关使用IDE接口读写硬盘的中文资料特别的少,虽然英文资料倒是挺全面,但是对于英语不好的我来说,看起来还是挺吃力的。一年半后,我又好奇的搜索的这方面的中文资料,结果依旧令人失望。于是,我就决定我把知道的IDE方面的知识写出来,一方面算是自己的一个学习笔记,另一方面也算是一种分享。我将这个系列定位为学习笔记,就是说,文章的很多地方都是自己的理解,不能保证所提到的知识都是正确的。所以如果这篇文章有幸被你看见,并且发现了问题,请使用email联系我。

IDE介绍

hdd-sata-pata-ide-aussie-pc-fix

IDE是Integrated Drive Electronics的简称,wiki上翻译的中文是“集成驱动电子设备”。我们可以认为它是一种接口,可以管理控制IDE的驱动器,比如硬盘,光驱等等。事实上,现在所谓的ATA/ATAPI接口的第一个版本的名称就叫做IDE,所以现在人们通常认为IDE就是PATA。如果现在去买主板,我们会发现集成PATA/IDE的主板已经消失了,现在主流主板都是使用的SATA,这是一套新的接口。那么,我们干嘛要学习一个已经淘汰的技术呢?其实不然,虽然硬件的接口被淘汰了,但是IDE的驱动器的控制模式还是存在的。现在的BIOS设置中,通常有一种叫做“legacy mode”或者“IDE”的选项,开启这个选项,系统就能如同操作IDE/PATA一样操作SATA了。而对于我这有写迷你内核的人来说,学习IDE是非常好的,因为虚拟机bochs模拟的硬盘设备就是IDE/PATA的,另一方面,把迷你内核拿到真机上做实验的时候,开启“legacy mode”或者“IDE”也能够很顺利的进行实验。

bios-sata-native-mode-ide-raid-ahci-ca184a

IDE通道以及通道寄存器地址

IDE有2个通道,可以管理4个驱动器,分别是:
通道1:
第一主驱动器
第一从驱动器
通道2:
第二主驱动器
第二从驱动器

每个通道都有两套用于控制其主从驱动器的寄存器,他们分别是 Control Block Registers 和 Command Block Registers。这些寄存器首先是有一个基础地址,然后通过按顺序可以获得整套寄存器地址,而寄存器的基础地址可以通过PCI Configuration Space来获得,更多情况下,我们不妨直接使用下面这张表来配置寄存器的基础地址。

2013-08-18_142953

事实上,这个基础地址是根据PCI IDE Controller模式不同而确定的。Compatibility模式下,寄存器的基础地址是固定的,但是在Native-PCI模式下,这个就需要读取具体的配置信息了。不过,大部分情况下,用上述地址不会有什么问题,所以这里就略过读取PCI Configuration Space的步骤了。

现在既然知道了寄存器的Base Address,那么下一步就是获得每个寄存器的地址了,其实这也非常简单。

Command Block Registers
1F0 (170)(读取和写入):数据寄存器
1F1 (171)(读):错误寄存器
1F1 (171)(写入):特性寄存器
1F2 (172)(读取和写入):扇区数寄存器
1F3 (173)(读取和写入):低LBA寄存器
1F4 (174)(读取和写入):中LBA寄存器
1F5 (175)(读取和写入):高LBA寄存器
1F6 (176)(读取和写入):驱动器/磁头寄存器
1F7 (177)(读):状态寄存器
1F7 (177)(写入):命令寄存器

Control Block Registers
3F6 (376)(读取):备用状态寄存器
3F6 (376)(写入):设备控制寄存器

另外还有一组寄存器叫做Bus Master IDE Register,我们使用DMA进行数据传输的时候会用到这类寄存器。现在就不去了解了,以免东西太多,造成不必要的混乱。

判断驱动器类型

前面说了很多的理论上的东西,现在我们看看怎么运用它们判断驱动器类型,比如是PATA还是SATA。当然,在做判断它们的类型之前,我们需要检测驱动器是否存在。判断方法很简单,先选择驱动器,对扇区数寄存器(1F2)和低LBA寄存器(1F3)写两个非0的数字,然后进行读取。如果读出的内容和写入的相同,那么我们可以认为驱动存在。就拿第一主驱动器举个例子:
mov dx, 1f6h ; 驱动器寄存器
xor al, al ; 选择驱动器,如果al第4位是0,那么选择0号设备,否则选择1号设备
out dx, al

mov dx, 1f2h
mov al, 55h ; 随意写一个数
out dx, al

mov dx, 1f3h
mov al, aah
out dx, al

mov dx, 1f2h
in al, dx ; 读取后比较
cmp al, 55h
jnz not_exist

mov dx, 1f3h
in al, dx
cmp al, aah
jnz not_exist

我用bochs测试的结果是,如果驱动器不存在,读出的数字总是0。接下来就可以获得驱动器的类型了,步骤是:1.选择驱动器(前面的操作以及做完这步了)2.软件复位驱动器。3.读取高LBA寄存器和中LBA寄存器。

mov dx, 3f6h ; 选择设备控制寄存器
mov al, 4h ; 设置第二位,表示软件复位
out dx, al

mov dx, 3f6h ; 选择设备控制寄存器
mov al, 0h
out dx, al

mov dx, 1f4h
in al, dx
mov id1, al

mov dx, 1f5h
in al, dx
mov id2, al

当ID1和ID2为不同的数值的时候,表示的设备不同,如下表所示
20130818152800
我在bochs实验的得到的结果是PATA,虽然模拟的设备比较老,但是这正是我想要的。

这样,我们读写硬盘的第一步,环境检测已经完成了。这个系列的下一篇文章,我们就来了解下通过PIO的方式读写硬盘。

MiniKernel

使用可编程间隔定时器(Programmable Interval Timer)编写系统时钟

很久没有写关于MiniKernel的文章了,这周末看着有点时间,就写一点关于定时器的东西吧。

8253

简单介绍

可编程间隔定时器(PIT)芯片(也就是我们常说的8253/8254芯片),他包含了1个振荡器,1个预分频器和3个独立的分频器。每个分频器有一个输出,它是可以让定时器控制外部电路(例如,IRQ0)

其中PIT的振荡器的频率是1.193182 MHz。具体为什么是这么个奇怪的数字,是有一点历史的,但这些不是这篇文章的重点,有兴趣的可以Google一下。

分频器也比较容易理解,就是把高频分割为低频,一般来说就是使用一个计数器,当每次脉冲的时候,计数器的数值减少,当计数器数值为0的时候,在输出上产生一个脉冲,并且计数器复位,重新开始计数。

PIT定时器的准确度依赖于所使用的振荡器,一般来说,一天的浮动为+/- 1.73秒。不过这种浮动,对我们影响并不大,所以也不必过于在意。

PIT的输出通道一共有三个:通道0,直接连接到IRQ0,并且触发时钟中断(这个通道是我们写MiniKernel最重要的一个。)。通道1,貌似以前是定时刷新内存的,但是现在没什么用了。通道2是连接到PC扬声器的,目前我也没有研究过它的作用。

这里再重点介绍一下通道0:PIT通道0的输出是连接到PIC芯片上的(8259A,以后有空也可以写一篇简单的介绍),因此,它能生成一个IRQ0的中断。通常情况下,在开机时,BIOS会将通道0的计数器的值设置为65535或0(其中如果是0,硬件会自动转化为65536),这样,它的输出频率就是18.2065Hz。另外,之所以说通道0最重要,主要原因就是它是三个通道中,唯一一个能连接到IRQ的,对于编写系统时钟至关重要。

编程相关

PIT是使用以下IO端口进行控制:

1
2
3
4
5
I/O 端口     用途
0x40         通道0的数据端口
0x41         通道1的数据端口
0x42         通道2的数据端口
0x43         控制字寄存器

控制字寄存器的具体内容如下:

8253cw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Bits
6 7选择通道:
0 0 =通道0
0 1 =通道1
1 0 =通道2
1 1 =回读命令(只有8254支持)
4 5访问模式:
0 0 =锁存计数值命令
0 1 =访问模式:读写最低有效字节
1 0 =访问模式:读写最高有效字节
1 1 =访问模式:先读写最低有效字节,然后读写高位字节
1-3工作模式:
0 0 0 =模式0(计数结束中断)
0 0 1 =模式1(硬件再触发单稳)
0 1 0 =模式2(速率发生器)
0 1 1 =模式3(方波发生器)
1 0 0 =模式4(软件触发闸门)
1 0 1 =模式5(硬件触发闸门)
1 1 0 =模式2(速率发生器,与010B相同)
1 1 1 =模式3(方波发生器,与011B相同)
0 BCD /二进制模式:0 = 16位二进制数,1 =四位BCD</blockquote>

这里,我们要写系统时钟,那么就应该这样选择:

  1. 选择通道,必须是通道0,那么bit 6 7 就分别为 0 0。
  2. 由于我们的计数器是16位的,那么访问模式bit 4 5就应该选择1 1。
  3. 数据模式,毫无疑问选择二进制模式。
  4. 最后就是工作模式了,应该选择什么呢?这里我也不想把这些模式都讲的很清楚,因为那样就涉及到引脚和电平等等硬件知识。现在我们只需要知道0,1,4,5这些模式都可以触发中断,但是却不会自动复位。只有模式2和3会自动复位。所以模式2 3都是我们可以用来作为系统时钟的模式。那么1-3 bits可以为0 1 0或者0 1 1。

例子:

现在假设,系统的时钟中断例程已经设置好了,并且设置好了PIC的IRQ0到这个例程。下面要做的事情就是,设置时钟中断的频率了。

1
2
3
4
5
6
7
mov dx, 1193180 / 100 ; 没10ms触发一次中断
mov al, 110110b ; 设置控制字寄存器,上面已经介绍过每个位的含意
out 0x43, al
mov ax, dx
out 0x40, al   ;先设置低位的值
xchg ah, al
out 0x40, al   ;再设置高位的值

总的来说8253 和 8254 是很有用的芯片。他们可以用在很多不同的设备,并用于很多不同的目的。不过就目前的PC来说,系统对他们的依赖已经不像以前那样严重了。随着科技的进步,APIC Timer已经可以取代他们。另外2005年,Intel和MS已经联合开发了新的高精度的定时器芯片High Precision Event Timer(HPET)。

虽然成熟的系统可能已经不用8253 和 8254了,但是对于我们自己的迷你内核来说,使用它们就完全足够了

MiniKernel

关于JsonCpp的中文编码问题

最近工作中用到了jsoncpp来解析json文件。但是遇到了一个这样的问题,如果json代码中有中文,并且用“\u594E\u6258\u65AF”这样的方式表示,那么jsoncpp解析的时候,就会把他转换成UTF-8。到这一步还是OK的,然后我就试图把这段中文写回文件,问题就来了,jsoncpp不会把中文转换为“\u594E\u6258\u65AF”这样的形式在存储,而是直接存储为UTF-8格式的文件。如图所示:

20130719231900

而我恰好是需要这种经过编码的形式的字符串,而非直接给我中文。在网上搜了搜,貌似也没有很好的解决方案,只好自己修改jsoncpp的代码以满足这个需求了。
简单读了下jsoncpp的read和write的代码。jsoncpp在read的时候会调用codePointToUTF8这个函数把\uXXXX这个形式的代码转换成UTF-8,但是write的时候就没有这样的转换了,虽然不是很了解作者这么写的思路,但是修改的思路倒是有了。我的做法是把所有需要两个和两个以上字节表示一个字符的UTF-8字符串全部转换成\uXXXX这个形式。那么就需要写一个转换函数。http://en.wikipedia.org/wiki/UTF-8 上很清楚的描述了这些转换关系,所以完全可以自己动手写一个这样的函数。以下是我自己的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int UTF8TocodePoint(const char *c, unsigned int *result)
{
int count = 0;
if (((*c) & 0x80) == 0) {
*result = static_cast<unsigned int>(*c);
count = 1;
}
else if (((*c) & 0xe0) == 0xc0) {
*result = static_cast<unsigned int>((((*c) & 0x1f) << 6) | ((*(c + 1)) & 0x3f));
count = 2;
}
else if (((*c) & 0xf0) == 0xe0) {
*result = static_cast<unsigned int>((((*c) & 0xf) << 12) | (((*(c + 1)) & 0x3f) << 6) | (((*(c + 2)) & 0x3f)));
count = 3;
}
else if (((*c) & 0xf8) == 0xf0) {
*result = static_cast<unsigned int>((((*c) & 0x7) << 18) | (((*(c + 1)) & 0x3f) << 12) | (((*(c + 2)) & 0x3f) << 6) | (((*(c + 3)) & 0x3f)));
count = 4;
}
else if (((*c) & 0xfc) == 0xf8) {
*result = static_cast<unsigned int>((((*c) & 0x3) << 24) | (((*(c + 1)) & 0x3f) << 18) | (((*(c + 2)) & 0x3f) << 12) | (((*(c + 3)) & 0x3f) << 6) | (((*(c + 4)) & 0x3f)));
count = 5;
}
else if (((*c) & 0xfe) == 0xfc) {
*result = static_cast<unsigned int>((((*c) & 0x1) << 30) | (((*(c + 1)) & 0x3f) << 24) | (((*(c + 2)) & 0x3f) << 18) | (((*(c + 3)) & 0x3f) << 12) | (((*(c + 4)) & 0x3f) << 6) | (((*(c + 5)) & 0x3f)));
count = 6;
}
return count;
}

然后把这个函数的调用加入到valueToQuotedString中,将原始的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( isControlCharacter( *c ) )
{
std::ostringstream oss;
oss << "\\u" << std::hex << std::uppercase << std::setfill('0') << std::setw(4) << static_cast<int>(*c);
result += oss.str();
}
else
{
result += *c;
}
break;

修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if ( isControlCharacter( *c ) )
{
std::ostringstream oss;
oss << "\\u" << std::hex << std::uppercase << std::setfill('0') << std::setw(4) << static_cast<int>(*c);
result += oss.str();
}
else if ((*c) & 0x80) {
unsigned int num = 0;
c += UTF8TocodePoint(c, &num) - 1;
std::ostringstream oss;
oss << "\\u" << std::hex << std::uppercase << std::setfill('0') << std::setw(4) << static_cast<int>(num);
result += oss.str();
}
else
{
result += *c;
}
break;

这样还没算结束,因为这个函数开头的地方还有一个判断,我们也需要修改一下,将原始代码:

1
2
3
4
5
if (strpbrk(value, "\"\\\b\f\n\r\t") == NULL && !containsControlCharacter( value ))
return std::string("\"") + value + "\"";

修改为:

1
2
3
4
5
if (strpbrk(value, "\"\\\b\f\n\r\t") == NULL && !containsControlCharacter( value ) && !containsMultiByte( value ))
return std::string("\"") + value + "\"";

containsMultiByte的实现是这样的:

1
2
3
4
5
6
7
8
9
10
11
static bool containsMultiByte( const char* str )
{
while ( *str )
{
if ( ( *(str++) ) & 0x80 )
return true;
}
return false;
}

好了,万事俱备,现在试一试效果,结果如图:

20130719232025

现在这个jsoncpp看起来已经满足了我的需求,但是不确定的是,不知道这样修改会不会引起其他问题。现在也只能说暂时不去管他,有问题再一步一步的修改吧。

Tips

调试挂死的Explorer

一个同事前几天告诉我说,他的explorer.exe总是挂死,不知道是什么情况导致的。于是我让他下次挂死的时候抓个dump我。抓Dump的工具很多,例如用Win7的TaskMgr,sysinternals的Procexp,或者Windbg本身。不过考虑到explorer挂死了,操作桌面起来不方便,所以最好选择能够自动检测挂死并且抓住dump的工具。这里比较推荐的是sysinternals的Procdump以及我开发的proc_dump_study(带UI)。

20130706003632

第二天,同事把explorer.exe挂死的Dump传给了我,200多MB。用Windbg打开Dump文件,第一反应就是看看有多少线程再说吧。

1
2
3
4
5
6
0:000> ~
. 0 Id: b14.b18 Suspend: 0 Teb: 7ffde000 Unfrozen
1 Id: b14.b1c Suspend: 0 Teb: 7ffdd000 Unfrozen
...
54 Id: b14.1a94 Suspend: 0 Teb: 7ff75000 Unfrozen
55 Id: b14.18c8 Suspend: 0 Teb: 7ff74000 Unfrozen

56个线程,肯定不能依次看栈回溯。按照尝试判断,explorer界面挂死,肯定是刷新界面的线程挂死了。所以栈回溯里肯定有explorer的身影。于是找找哪个线程有explorer模块。

1
2
3
4
5
6
7
8
9
10
11
0:000> !findstack explorer!
Thread 000, 2 frame(s) match
* 04 001bf924 0087aa50 explorer!wWinMain+0x54a
* 05 001bf9b8 75771154 explorer!_initterm_e+0x1b1
Thread 003, 2 frame(s) match
* 11 0324f714 008757a6 explorer!CTray::_MessageLoop+0x265
* 12 0324f724 75b346bc explorer!CTray::MainThreadProc+0x8a
Thread 008, 1 frame(s) match
* 03 04a1fc0c 75b346bc explorer!CSoundWnd::s_ThreadProc+0x3a

从上面的结果看来,3号线程最可疑,于是看看完整的堆栈情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:003> kv
# ChildEBP RetAddr Args to Child
00 0324f508 76eb5aec 75236924 00000002 0324f55c ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 0324f50c 75236924 00000002 0324f55c 00000001 ntdll!NtWaitForMultipleObjects+0xc (FPO: [5,0,0])
02 0324f5a8 7576f10a 0324f55c 0324f5d0 00000000 KERNELBASE!WaitForMultipleObjectsEx+0x100 (FPO: [Non-Fpo])
03 0324f5f0 75fa90be 00000002 7ffdf000 00000000 kernel32!WaitForMultipleObjectsExImplementation+0xe0 (FPO: [Non-Fpo])
04 0324f644 73d51717 000002fc 0324f678 ffffffff user32!RealMsgWaitForMultipleObjectsEx+0x13c (FPO: [Non-Fpo])
05 0324f664 73d517b8 000024ff ffffffff 00000000 duser!CoreSC::Wait+0x59 (FPO: [Non-Fpo])
06 0324f68c 73d51757 000024ff 00000000 0324f6b8 duser!CoreSC::WaitMessage+0x54 (FPO: [Non-Fpo])
07 0324f69c 75fa949f 000024ff 00000000 0324f68c duser!MphWaitMessageEx+0x2b (FPO: [Non-Fpo])
08 0324f6b8 76eb60ce 0324f6d0 00000008 0324f7e8 user32!__ClientWaitMessageExMPH+0x1e (FPO: [Non-Fpo])
09 0324f6d4 75fa93f3 00851dee 00000000 80000000 ntdll!KiUserCallbackDispatcher+0x2e (FPO: [0,0,0])
0a 0324f6d8 00851dee 00000000 80000000 00901180 user32!NtUserWaitMessage+0xc (FPO: [0,0,0])
0b 0324f714 008757a6 00000000 75b318f2 0324f7ac explorer!CTray::_MessageLoop+0x265 (FPO: [Non-Fpo])
0c 0324f724 75b346bc 00901180 00000000 00000000 explorer!CTray::MainThreadProc+0x8a (FPO: [Non-Fpo])
0d 0324f7ac 75771154 001bf810 0324f7f8 76ecb299 shlwapi!WrapperThreadProc+0x1b5 (FPO: [Non-Fpo])
0e 0324f7b8 76ecb299 001bf810 75d00467 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo])
0f 0324f7f8 76ecb26c 75b345e9 001bf810 00000000 ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo])
10 0324f810 00000000 75b345e9 001bf810 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])

可以看出线程正在调用WaitForMultipleObjectsEx等待两个内核对象。那么看看这两个内核对象是什么吧。

1
2
3
4
5
6
7
8
0:003> dp 0324f55c L2
0324f55c 00000318 000002fc
0:003> !handle 00000318
Handle 00000318
Type Event
0:003> !handle 000002fc
Handle 000002fc
Type Event

很不幸,两个内核对象都是Event,这样就没有什么可参考的价值了,因为我们没办法知道谁应该去设置两个event。那么好吧,从其他方面下手,看能不能发现问题。看看关键区的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:003> !cs -l
-----------------------------------------
DebugInfo = 0x76f47540
Critical section = 0x76f47340 (ntdll!LdrpLoaderLock+0x0)
LOCKED
LockCount = 0x6
WaiterWoken = No
OwningThread = 0x000013e0
RecursionCount = 0x1
LockSemaphore = 0x220
SpinCount = 0x00000000
-----------------------------------------
DebugInfo = 0x002ac6e0
Critical section = 0x765ea0f0 (shell32!CMountPoint::_csDL+0x0)
LOCKED
LockCount = 0x0
WaiterWoken = No
OwningThread = 0x000013e0
RecursionCount = 0x1
LockSemaphore = 0xA50
SpinCount = 0x00000000

看到一个很可疑的情况了,两个cs都被一个线程占用,更可疑的是这个线程居然还占用了LdrpLoaderLock。这就很有可能是引起死锁的原因了。来看看这个线程的完整堆栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
0:053> ~~[13e0]s
eax=06d02f00 ebx=00000000 ecx=03920000 edx=06ce0000 esi=000015ac edi=00000000
eip=76eb6194 esp=0b1ad1cc ebp=0b1ad238 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!KiFastSystemCallRet:
76eb6194 c3 ret
0:053> k
# ChildEBP RetAddr
00 0b1ad1c8 76eb5b0c ntdll!KiFastSystemCallRet
01 0b1ad1cc 7523179c ntdll!ZwWaitForSingleObject+0xc
02 0b1ad238 7576efe3 KERNELBASE!WaitForSingleObjectEx+0x98
03 0b1ad250 7576ef92 kernel32!WaitForSingleObjectExImplementation+0x75
04 0b1ad264 7622399a kernel32!WaitForSingleObject+0x12
05 0b1ad294 7622299c shell32!CMountPoint::_InitLocalDrives+0xcd
...
14 0b1ad81c 762aa690 shell32!SHGetFolderLocation+0x121
15 0b1ad838 0a5c0bf5 shell32!SHGetSpecialFolderLocation+0x17
WARNING: Stack unwind information not available. Following frames may be wrong.
16 0b1ae0bc 0a5bfceb HaoZipExt!DllUnregisterServer+0x1cd04
17 0b1ae240 0a5e0200 HaoZipExt!DllUnregisterServer+0x1bdfa
18 0b1ae284 0a5e02b9 HaoZipExt!DllUnregisterServer+0x3c30f
19 0b1ae2ac 76ecfbdf HaoZipExt!DllUnregisterServer+0x3c3c8
1a 0b1ae3a0 76ed008b ntdll!LdrpRunInitializeRoutines+0x26f
1b 0b1ae50c 76ecf499 ntdll!LdrpLoadDll+0x4d1
1c 0b1ae540 7523b96d ntdll!LdrLoadDll+0x92
1d 0b1ae57c 7534a333 KERNELBASE!LoadLibraryExW+0x1d3
1e 0b1ae598 7534a2b8 ole32!LoadLibraryWithLogging+0x16
...
39 0b1afbac 76241ee6 shell32!CShellExecute::_DoExecute+0x5a
3a 0b1afbc0 75b346bc shell32!CShellExecute::s_ExecuteThreadProc+0x30
3b 0b1afc48 75771154 shlwapi!WrapperThreadProc+0x1b5
3c 0b1afc54 76ecb299 kernel32!BaseThreadInitThunk+0xe
3d 0b1afc94 76ecb26c ntdll!__RtlUserThreadStart+0x70
3e 0b1afcac 00000000 ntdll!_RtlUserThreadStart+0x1b

首先一眼就看到了一个非系统模块HaoZipExt。再扫一眼,发现他在LdrpLoaderLock的时候又去等待了某个内核对象。那么再来看看这个内核对象是什么吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:053> kv L5
# ChildEBP RetAddr Args to Child
00 0b1ad1c8 76eb5b0c 7523179c 000015ac 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 0b1ad1cc 7523179c 000015ac 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc (FPO: [3,0,0])
02 0b1ad238 7576efe3 000015ac ffffffff 00000000 KERNELBASE!WaitForSingleObjectEx+0x98 (FPO: [Non-Fpo])
03 0b1ad250 7576ef92 000015ac ffffffff 00000000 kernel32!WaitForSingleObjectExImplementation+0x75 (FPO: [Non-Fpo])
04 0b1ad264 7622399a 000015ac ffffffff 00000000 kernel32!WaitForSingleObject+0x12 (FPO: [Non-Fpo])
0:053> !handle 000015ac f
Handle 000015ac
Type Thread
Attributes 0
GrantedAccess 0x1fffff:
Delete,ReadControl,WriteDac,WriteOwner,Synch
Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
HandleCount 4
PointerCount 7
Name <none>
Object specific information
Thread Id b14.1a94
Priority 10
Base Priority 0

原来他在等待1a94这个线程结束啊,那么这个1a94线程又在干嘛呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0:054> ~~[1a94]s
eax=0bbdfbb4 ebx=00000000 ecx=00000000 edx=00000000 esi=76f47340 edi=00000000
eip=76eb6194 esp=0bbdfa24 ebp=0bbdfa88 iopl=0 nv up ei pl nz ac pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000217
ntdll!KiFastSystemCallRet:
76eb6194 c3 ret
0:054> kv
# ChildEBP RetAddr Args to Child
00 0bbdfa20 76eb5b0c 76e9f98e 00000220 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
01 0bbdfa24 76e9f98e 00000220 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc (FPO: [3,0,0])
02 0bbdfa88 76e9f872 00000000 00000000 00000000 ntdll!RtlpWaitOnCriticalSection+0x13e (FPO: [Non-Fpo])
03 0bbdfab0 76ecb31d 76f47340 7d4908db 7ff75000 ntdll!RtlEnterCriticalSection+0x150 (FPO: [Non-Fpo])
04 0bbdfb44 76ecb13c 0bbdfbb4 7d49080f 00000000 ntdll!LdrpInitializeThread+0xc6 (FPO: [Non-Fpo])
05 0bbdfb90 76ecb169 0bbdfbb4 76e70000 00000000 ntdll!_LdrpInitialize+0x1ad (FPO: [Non-Fpo])
06 0bbdfba0 00000000 0bbdfbb4 76e70000 00000000 ntdll!LdrInitializeThunk+0x10 (FPO: [Non-Fpo])

原来这个线程在等待LdrpLoaderLock这个锁啊,真相大白了。这里理一下思路,线程13e0,创建后,调用Loadlibrary,装载HaoZipExt。这个时候HaoZipExt获得LdrpLoaderLock,但是HaoZipExt犯了编写DLL的大忌。在DLLMain里面做了一些不能预期的事情。HaoZipExt调用了SHGetSpecialFolderLocation,这个函数在内部会创建一个线程,运行一个叫做FirstHardwareEnumThreadProc 的子过程。这个线程起来之后,就会通知所用的DllMain,告诉他们DLL_THREAD_ATTACH的消息。但是告诉他们这个消息之前,首先要获得LdrpLoaderLock这个锁。但是LdrpLoaderLock这个锁正在被创建他的线程使用,而且还在等自己结束,就这样死锁了。这也是MSDN特别强调告诉我们,不要在DllMain里有过多自己不能预期的操作的原因。

那么看看这个罪魁祸首是什么模块吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0:054> lmvm HaoZipExt
Browse full module list
start end module name
0a5a0000 0a605000 HaoZipExt (export symbols) HaoZipExt.dll
Loaded symbol image file: HaoZipExt.dll
Image path: C:\Program Files\HaoZip\HaoZipExt.dll
Image name: HaoZipExt.dll
Browse all global symbols functions data
Timestamp: Wed Jul 25 17:16:06 2012 (500FB956)
CheckSum: 0006C14B
ImageSize: 00065000
File version: 3.0.1.9002
Product version: 3.0.1.9002
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0804.04b0
CompanyName: 瑞创网络
ProductName: 2345好压(HaoZip)
InternalName: HaoZipExt
OriginalFilename: HaoZipExt.dll
ProductVersion: 3.0
FileVersion: 3.0.1.9002
FileDescription: 2345好压-Windows扩展模块
LegalCopyright: 版权所有(c) 2012 瑞创网络
Comments: www.haozip.com

知道问题后,我感觉这应该就是explorer挂死的原因,虽然没有100%的证据,但是至少也是一个造成死锁的程序,早卸载为妙,于是我告诉了同事,卸载了这个叫做好压的软件。之后几天,explorer运行正常,再也没有出现过挂死现象了。

最后总结HaoZipExt犯的错误
1.在DllMain里面的做了线程创建的操作。
2.跟挂死无关,只是吐槽一下他在DllMain里面调用了SHGetSpecialFolderLocation这个函数。因为这个函数已经不被支持,而且有可能在将来被废弃。以下是MSDN的原话:[SHGetSpecialFolderLocation is not supported and may be altered or unavailable in the future. Instead, useSHGetFolderLocation.]

感叹一下,写一个健壮的程序真的不是件容易的事啊。

Debugging

关于WOW64的一点记录

1.关于TEB的地址:32位的TEB地址在64位TEB地址加上0x2000的偏移处。验证如下:

1
2
3
4
5
6
7
8
9
10
11
0:000> r @$teb
$teb=000000007efdb000
0:000:x86> dg @fs
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0053 7efdd000 00000fff Data RW Ac 3 Bg By P Nl 000004f3
0:000:x86> ? 7efdd000 - 7efdb000
Evaluate expression: 8192 = 00002000

2.从32位切换到到64位的时候,系统会保存32位的寄存器状态。这些状态保存在Teb->TlsShots[1]中。继续用Windbg验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:000> dt _teb @$teb -a5 TlsSlots
ntdll!_TEB
+0x1480 TlsSlots :
[00] (null)
[01] 0x00000000`001cfd20 Void
[02] (null)
[03] 0x00000000`001ca930 Void
[04] (null)
0:000> !wow64exts.r
No wow64 context address specified, dumping wow64 context from cpu area...
Teb64 Address: 0x7efdb000, CpuArea Address: 0x1cfd20
Context Address: 0x1cfd24
eax=00000000 ebx=00000000 ecx=00000000 edx=00000000 esi=77200094 edi=00000000
eip=772000a6 esp=000ee0ac ebp=000ee150 iopl=0 nv up ei pl zr na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246

3.从64位切换到到32位的时候,会保存64位的RSP,保持的地址是Teb->TlsShots[0]。切换回64位的时候,这个地址被清0。

1
2
3
4
5
6
7
0:000> dt ntdll!_TEB @r12 -a5 TlsSlots
+0x1480 TlsSlots :
[00] 0x00000000`001ce530 Void
[01] 0x00000000`001cfd20 Void
[02] (null)
[03] (null)
[04] (null)

NTInternalsTips

EtwLogView —— 实时查看ETW的工具

最近玩XPerf玩的比较多,也经常和cradiator(blog)讨论xperf和etw的话题。上周吃饭的时候就讨论到,貌似没找到一款实时记录查看etw的工具。当时我的观点是,只是看etw的原始记录很难分析出什么东西,必须配合很细工具,例如xperfview。这样才能有效的发挥出etw的威力,所以实时工具用处不大。而cradiator认为,除了这些常规的用法外,如果能实时记录查看etw的信息,那么把etw当作平时的log输出方式也是不错的选择。这样的好处就是,不需要额外的加入log机制,使用etw就足够强大,其次如果遇上了问题,这些etw的记录又可以作为event,帮助xperfview的分析。然后这家伙就怂恿我写一个:-)

经过上面的一番介绍,应该就能知道EtwLogView的用处何在了。他是一个“实时”记录查看etw的工具。之所以用上了引号。是因为这个实时是有不确定性的。例如,如果一个provider输出了大量的事件信息。那么这个工具就会遇上麻烦,因为更新记录,和刷新界面的速度很可能跟不上provider的输出速度。这样,这个实时就大打折扣。不过就像cradiator所说的,只用来监控自己的事件,倒没什么问题。

现在EtwLogView是1.0版本,勉强算是可以先用着吧。

首先需要在Windows7系统和管理员权限运行工具,然后就可以创建Session,创建的时候需要选择Session的Provider。可以在List选择,也可以自己输入(必须为GUID格式)。如果想监视多个Provider,那么每个GUID之间需要用分号隔开。

20130613010933

20130613011032

另外,如果想更灵活的设置Session,可以使用xperf创建Session。然后打开EtwLogView,选择打开Session。在文本框中输入Session名,如果有多个Session需要监视,那么可以用分号隔开Session名。

20130613011054

ETW输出的信息很多,我主要列出了12列,并且可以根据自己的需要选择显示的列。

20130613011110

目前的功能就这么多,如果真的有的上再来看看能加上哪些功能吧。

下载EtwLogView

Debugging

几个有趣的未文档化Windbg的扩展命令

这里使用的是Windbg最新版本,版本号是6.2.9200,可以在Windows8的SDK中获得。

1.eflags 用更加友好的方式显示被设置的标志寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
0:000> r efl
efl=00000246
0:000> !eflags
BIT_1_RESERVED
PARITY_FLAG
ZERO_FLAG
INTERRUPTS_ENABLED
0:000> r zf
zf=1
0:000> r if
if=1

2.frame 用module!function的方式设置栈帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:000> kn L3
# ChildEBP RetAddr
00 0018df2c 7586d7db kernel32!CreateFileW
01 0018e024 7586d9d1 apphelp!IdentifyCandidates+0x176
02 0018e054 7586d87b apphelp!ApphelpQueryExe+0xb8
0:000> .frame
00 0018df2c 7586d7db kernel32!CreateFileW
0:000> !frame apphelp!ApphelpQueryExe
Frame Set to 0x00000002
0:000> .frame
02 0018e054 7586d87b apphelp!ApphelpQueryExe+0xb8

3.hashblob 计算指定内存的hash,hash方式包括md5和sha1

1
2
3
4
5
6
7
8
0:000> !hashblob
Not enough parameters 0
!hashblob <hash> <Start> <End>
<hash>: 1 for MD5
<hash>: 2 for SHA1
0:000> !hashblob 1 001f0000 001f0100
DCC4E0B6659F6887DEC24A9FF2D57DC8

4.imports 列出指定模块的导入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0:000> !imports notepad
notepad notepad Imports from file: ADVAPI32.dll
RegSetValueExW
RegQueryValueExW
...
notepad Imports from file: KERNEL32.dll
FindNLSString
GlobalAlloc
GlobalUnlock
GlobalLock
GetTimeFormatW
GetDateFormatW
GetLocalTime
...

5.inframe 找出指定地址所在的栈帧范围

1
2
3
4
5
6
7
ChildEBP RetAddr
0018df2c 7586d7db kernel32!CreateFileW
0018e024 7586d9d1 apphelp!IdentifyCandidates+0x176
0:000> !inframe 0018df8c
0018df8c 0 00001714 0018df34 < 0018df8c < 0018e02c
Frame: 1

6.inmodule 找出指定地址所在的模块

1
2
0:000> !inmodule 7586d9d1
0x7586d9d1: apphelp!ApphelpQueryExe

7.url 用默认浏览器打开指定网页

1
2
3
4
5
0:000> !url
Please provide a valid URL (http://... or https://... )
USAGE: !url <url>
0:000> !url http://0cch.net

Tips

关于DLL加载和运行的性能优化总结

本文从三个方面总结了加快DLL加载和运行速度的方法,他们分别是:

  1. 使用Rebase和Bind。
  2. 按序号的方式导入函数。
  3. 使用预读取技术(chromium加载dll时使用了此项)。

首先,讨论一下使用Rebase和Bind,提高DLL加载和运行性能的方法。我们知道,在编译链接DLL的时候,连接器会给DLL一个加载基址,而这个加载基址对于DLL模块中的使用了硬编码地址的代码和数据是至关重要的。因为这些硬编码的地址都是连接器通过DLL加载基址计算出来的。

这样,当我们的DLL被加载到进程空间当中的时候,如果足够幸运的话,DLL正好被加载到连接器所指定的地址,那么那些硬编码的地址就是完全正确的,加载过程不需要其他额外的工作就能让DLL正确运行了。

但是,真实的情况并不是这么理想,因为我们的进程可能不得不加载许许多多DLL,这样就会导致,某些DLL预定的基址可能已经被其他DLL使用。因此,前者就不得不加载到其他地址。由此带来的影响也是显而易见的,硬编码的地址就会出错。为了不让这种错误发生,加载器也就不得不多做一些工作,就是重新矫正这些硬编码。这样,如果DLL里包含的硬编码地址越多,加载的速度就会越慢,从而导致DLL加载性能下降。

另一方面,由于DLL加载基址是不可预见的,所以DLL加载的时候,加载器会根据导入表搜索所有的导入函数,计算并且确定导入函数的正确地址。这个过程也会对DLL加载性能有一定影响。

微软已经考虑到了这个问题的优化方法,在SDK中为我们提供了Rebase和Bind工具。其中Rebase工具,可以合理安排进程中DLL的加载地址,并且修改到PE中,从而避免DLL加载时Rebase操作。这样,一旦我们确认了DLL的加载基址是不会发生改变,那么我们就可以使用Bind工具将导入表进行绑定,这样的好处就是加载器不需要在加载的时候去计算确定导入函数地址,因为这些地址以及被预设了。

需要注意的一点,即使你做了这么多的事情,也不能完全避免加载基址的冲突,例如使用ASLR的DLL,其加载的地址是会发生变化的。所以就不能保证Rebase和Bind的有效性。具体能优化多少性能,在不同的案例中可能结果不同,需要具体实验才能知道。

第二种优化DLL运行性能的方案就是使用序号而非名称的方式导入函数。这一点也就非常容易解释了,如果通过函数名确定导入函数地址,那么加载器就不得不对字符串进行比较,从而确定正确的函数地址,虽然DLL的导出表是按顺序字母排列的,并且查找方式也是二分查找,但是如果函数很多,这依然是个耗时的工作。例如,我机器上的MFC100.dll有14000多个导出函数,如果进程需要按照名称确定自己需要的函数是哪一个,那么工作量还是不小的吧。所以MFC100.dll很明智的使用了导出序号的方法,这样加载器计算导入函数的时候,就能够使用序号来确定函数地址了,也就是简单使用数组搜索,从而得到目标函数地址。

这种方法的优化效果同样也要根据具体情况而定,如果导入函数少,目标DLL导出的函数也非常少,那么这种优化应该是没有什么意义的。相反,如果需要导入函数很多,而且目标DLL也导出了很多函数,那么在想提高程序加载性能的时候,不妨试一试这个方案。

最后一个方法是Pre-Read技术,使用在chromium中的。这种方法的原理是将DLL预先存入系统缓存,从而减少Page Fault来达到提高性能的目的。这种优化主要针对的是进程冷启动加载DLL的情况。进程第一次启动的时候,加载所需的DLL,DLL会被MAP到内存空间,虽然如果查询这片MAP的内存,会发现确实是COMMIT状态。但是,实际上系统并没要保证这些内存在Working Set中。一般情况下,系统只会把你想要用到的内存加载到Working Set中,以节约物理内存。这样,当我们每次用到这个并没有对应的Working Set的虚拟内存的时候,就发生了Page Fault,系统这个时候才会把这些内存加载到Working Set。而Page Fault对性能的影响很是比较大的。所以Pre-Read技术就有了用武之地。

Chromium对Pre-Read实现的非常好,代码的具体位置是http://src.chromium.org/viewvc/chrome/trunk/src/chrome/app/image_pre_reader_win.cc 。代码中,分别针对XP和XP以上的系统使用了不同的方法让系统缓存目标DLL。在XP以上的系统中,代码简单的通过ReadFile将文件读取到内存中,然后释放内存,关闭文件句柄,就可以达到缓存目标DLL的目的。而在XP系统下,做法有些不同,它使用LoadLibraryEx函数,将文件Load到内存空间,然后尝试对每个Page进行读取操作,已达到让数据载入Working Set的目的。

以上是我所知道的三种加载DLL的优化方法,也可能还有更多更好的方法,有兴趣的可以一起讨论下。不过无论什么性能优化方法,都必须建立在实际项目的基础上,并且有科学的依据和论证,切不可只从理论上下结论来优化程序,纸上谈兵,这样很可能适得其反,让性能变得更糟。(例如这个例子里所提到的,看上去不错的优化可能还不如什么都不做:http://blogs.msdn.com/b/oldnewthing/archive/2004/12/17/317157.aspx)。至于说,如何得到程序运行数据用于总结出有效的优化方案,这里强烈推荐一款神器XPerf。实际上,也是因为使用了XPerf,才让我对性能优化产生了浓厚的兴趣!

DebuggingNTInternals

gdi_handle_study —— 查看进程GDI资源情况的工具

gdi_handle_study 是一个用于查看进程中gdi句柄资源的工具。可以用于监控gdi资源是否泄露,已经对gdi资源的使用情况。使用方法非常简单:

1
2
3
4
5
6
<blockquote>usage: gdi_handle_study.exe [-c] [-v [-f <filter>]] [processname|pid]
processname    List GDI handles loaded by process (partial name accepted)
pid                   List GDI handles associated with the specified process id
-c                    Show GDI count information.
-v                    Show GDI handle information.
-f                    Filter the GDI handle type.</blockquote>

在不用任何参数的情况下,工具会显示所有进程的gdi资源使用概况,如图所示:
20130504154700

值得注意的是,GDI Total和GDI All的区别在于,GDI Total统计出来的数量,是通过工具本身枚举可统计GDI资源后得出统计值,而GDI All是通过系统API直接获得的值,有些的情况下,GDI Total的值是小于GDI All的值的。这种情况可能因为某些GDI资源是系统保留的。另外一个要注意的是,如果要显示所有进程的gdi情况,需要有管理员权限运行该工具。

processname和pid参数能让我们指定需要查看的进程名或者进程ID。参数-c能查看更为详细的gdi资源的统计情况。如下图所示:
20130504155533

从上图可以看出,qq这种DirectUI程序,用的Bitmap资源何其的多啊。。。

-v参数是用来查看更为详细的GDI资源信息,其中就包括额资源的句柄,资源的种类以及资源的内核对象地址。如图所示:
20130504160314

最后工具还能利用-f filter,来查看想看到的资源情况,例如上图中,bitmap不是自己想看的资源,但是却占据了大量的视野。这个时候filter就能用上了。如图:
20130504160837

上图就是利用filter,显示的Brush资源的详细情况了。

下载gdi_handle_study

NTInternals

ProcMem —— 进程内存查看工具

ProcMem是一个进程内存的查看工具,他可以显示进程中的内存分配情况,以及内存大概的用途,并且Dump指定的内存模块。工具界面如下图

20130421231145

ProcMem并不是实时监控目标进程的内存情况,而是对内存情况作了一次快照和统计,并且显示出来。所以想看到进程最新的内存状态,可以点击Refresh菜单。

工具上半部分就是显示的目标进程的内存分布情况,以及一些细节信息。这里必须要谈到一点,Windows的标准控件中没有TreeList,对我这个写100个程序99个没有界面的人来说,自绘这个东西差点没要了我的命。

工具的下半部分用来显示TreeList选中项的内存情况,十六进制表示。值得注意的是,这里只会显示选中内存头个PAGE_SIZE大小的内存情况。如果想查看该项内存的全部情况,可以使用Dump功能,把内存Dump下来,然后用WinHex这样的工具查看,这个简单的内存显示区,只是为了提供一个预览功能而已。

值得一提的是,菜单Find,不是用来查找下方十六进制内存显示的内容,而是用来查找TreeList中的项目。例如想找到有关ntdll的内存区域,可以在查找框中输入ntdll,这样就可以定位如图所示的项目了。

这个工具是我花了大半周的业余时间弄的,时间比较仓促,不可避免的可能会有些bug。如果你刚好用上了这个工具,而且发现了bug,不妨通过邮件联系我(邮箱地址见About Me页面)。

下载ProcMem(包括32和64位版本)

DebuggingNTInternals