Windows7下控制单个进程音量的小技巧

2013-09-10_200213

如上图所示,Windows7下有一个很有趣的功能,就是可以给单独的进程调节音量。出于好奇,在网上翻了下资料,原来这个功能要归功于Windows7上新的音频接口——Core Audio APIs。这套API是用COM写,各种接口也比较多。但是如果我们的目的只是控制单个进程的音量,那还是很简单的。接下来的代码就是控制进程音量的函数了。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioSessionManager2 = __uuidof(IAudioSessionManager2);
const IID IID_IAudioSessionControl2 = __uuidof(IAudioSessionControl2);
const IID IID_ISimpleAudioVolume = __uuidof(ISimpleAudioVolume);

BOOL SetProcessVolume(ULONG target_pid, float level)
{
CComPtr imm_dev_enumor;

HRESULT hr = imm_dev_enumor.CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL);

if (FAILED(hr)) {
return FALSE;
}

CComPtr imm_dev;
hr = imm_dev_enumor->GetDefaultAudioEndpoint(eRender, eMultimedia, &imm;_dev.p);
if (FAILED(hr)) {
return FALSE;
}

CComPtr session_mgr2;
hr = imm_dev->Activate(IID_IAudioSessionManager2, CLSCTX_ALL, NULL, (void **)&session;_mgr2.p);
if (FAILED(hr)) {
return FALSE;
}

CComPtr session_enumor;
hr = session_mgr2->GetSessionEnumerator(&session;_enumor.p);
if (FAILED(hr)) {
return FALSE;
}

int count;
if (FAILED(hr)) {
return FALSE;
}

hr = session_enumor->GetCount(&count;);
for (int i = 0; i < count; i++) {
CComPtr session_ctrl;
hr = session_enumor->GetSession(i, &session;_ctrl.p);
if (FAILED(hr)) {
continue;
}

CComPtr session_ctrl2;
hr = session_ctrl->QueryInterface(IID_IAudioSessionControl2, (void **)&session;_ctrl2.p);
if (FAILED(hr)) {

}

ULONG pid;
hr = session_ctrl2->GetProcessId(&pid;);
if (FAILED(hr)) {
continue;
}


if (pid != target_pid) {
continue;
}

CComPtr simple_vol;
hr = session_ctrl2->QueryInterface(IID_ISimpleAudioVolume, (void **)&simple;_vol.p);
if (FAILED(hr)) {
continue;
}

simple_vol->SetMasterVolume(level, NULL);
}

return TRUE;
}

Tips

使用PCI IDE Controller读写硬盘 – 2

20130901011808

上一篇文章简单介绍了用PIO的方式读写硬盘数据,那么这篇文章就来介绍另一种数据传输的方式——DMA。

DMA全称是Direct memory access,以下依旧是wiki上的一段简短的介绍:
“直接存储器访问(Direct Memory Access,DMA)是计算机科学中的一种内存访问技术。它允许某些电脑内部的硬件子系统(电脑外设),可以独立地直接读写系统存储器,而不需绕道中央处理器(CPU)。很多硬件的系统会使用DMA,包含硬盘控制器、绘图显卡、网卡和声卡。”

结合以上的描述和上一篇PIO的介绍,我们就可以发现DMA的优势,他最大的优势之一就是解放了CPU,让CPU不用重复的执行IO端口的操作读写数据。使用DMA的时候,CPU可以做其他的计算,读写数据的操作完全交由CPU外部的DMA芯片进行操作。当读写操作结束后CPU收到通知,然后再来处理读写之后的工作。DMA的另一个优势,就是速度快,不过这么说也不是完全正确的。因为古老的ISA DMA的速度只有4MB/s,现代CPU跑起PIO来,传输速度应该会比这个快。幸运的是,硬盘使用的DMA并不是ISA DMA,而是PCI DMA。PCI DMA的速度通常都超过了100MB/s,所以说速度也算是DMA的一个优势了吧。这里在顺便提一点,ISA DMA也不是完全没有用处的。软盘使用的DMA就是ISA DMA,虽然说软盘在现代的PC上已经消失了,但是如果要写自己的Mini Kernel,那么支持软盘以及ISA DMA还是很有必要的。

DMA的优势很明显,付出的代价就是编程起来相对复杂。那么下面就来介绍让IDE使用DMA传输数据的基础知识。

物理区域描述符(Physical Region Descriptor)
20130828165621进行数据传输的物理内存块都用物理区域描述符进行描述。当所有在物理区域描述符表中的物理区域描述符所指向的内存都被传输完成后,数据传输就会停止。每个物理区域描述符是8字节。前4个字节指定的是物理内存区域的地址。接下来的两个字节指定内存数量。最后一个字节的第7位表示此理区域描述符是该表中最后一个描述符。

物理区域描述符表(Physical Region Descriptor Table)
这张表中包含一定数量的物理区域描述符(PRD),描述符表必须是4字节对齐且不能跨越64K边界的内存。

总线主控IDE寄存器(Bus Master IDE Register)
20130829115331

要获得总线主控IDE寄存器的基础地址,需要读取PCI配置空间IDE区域的0x20处的DWORD。由于这篇文章不会设计到如何读取PCI配置空间,所以这里的基址就采用bochs设定(0xC000)。后面代码部分也会直接硬编码。

总线主控IDE命令寄存器(Bus Master IDE Command Register)
20130829120041

这里读写控制位是特别要注意的,刚开始容易理解错误。这里的读写是针对的设备,而不是CPU。也就是说这里的都,是指设备读取CPU指定的内存到自己的数据空间。而写是指将自己的数据空间的数据写到CPU指定的内存。所以这里的读写和我们对硬盘要做的读写是刚好相反的。

总线主控IDE状态寄存器(Bus Master IDE Status Register)
20130829120248

描述符指针寄存器(Descriptor Table Pointer Register)
用于设置物理区域描述符表的地址

对于初学者理论知识不用了解的过细,最好还是在代码中边写边学习,还是一边堆代码,一边解释吧。
(关于下面代码的补充说明:由于使用DMA必须处理中断以获得DMA处理结束的信号,而配置中断又涉及到许多理论知识和额外代码(8259A &IDT),所以下面的代码就不涉及配置中断了,我这里就假设CPU已经进入保护模式,但没有开启分页并且IDE的中断已经配置完毕了。以下代码依旧为了保持最简洁,忽略了状态和结果的检查,在试验中够用即可)

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
mov dx, 0C000h ; 设置开始停止位为0,停止DMA
mov al, 0h
out dx, al

mov dx, 0C002h ; 清除中断位和错误位,这里清除方式比较特别,设置1后清除
mov al, 6h
out dx, al

; 配置描述符表,表地址为10000h,且只有一个描述符
; 描述符描述的物理基址是20000h,大小为512字节,且设置了第7位,
; 说明自己就是最后一个描述符
mov dword ptr [10000h], 20000h
mov dowrd ptr [10004h], 200h | 80000000h
mov dx, 0C004h
mov eax, 10000h
out dx, eax

mov dx, 3f6h ; 这里不再设置nIEN,DMA需要中断
mov al, 0h
out dx, al

mov dx, 1f1h ; 下面代码基本上和PIO一致,
mov al, 0 ; 详细注释请看上一篇文章
out dx, al

mov dx, 1f2h
mov al, 1
out dx, al

mov dx, 1f3h
mov al, 11h
out dx, al

mov dx, 1f4h
mov al, 22h
out dx, al

mov dx, 1f5h
mov al, 33h
out dx, al

mov dx, 1f6h
mov al, 44h
out dx, al

mov dx, 1f7h ; 设置读取扇区的命令C8h,不同于20h,这个是DMA读取扇区的命令
mov al, C8h
out dx, al

mov dx, 0C000h ; 设置开始停止位为1,开始DMA,并且指定为读取硬盘操作
mov al, 9h ; (对硬盘而言是写出,所以设置bit3)
out dx, al

call wait_int ; 等待中断

mov dx, 0C000h ; 中断返回,设置开始停止位为0,停止DMA
mov al, 0h ; 如果一切都顺利,那么20000h开始的512个字节
out dx, al ; 就应该是读出的硬盘数据了

上面的代码主要分为以下这几步:
1) 在系统内存中配置PRD Table。每个PRD是8个字节,其中包含着一个起始内存地址和一个内存大小传送,而且PRD必须是4字节对齐的。
2) 给PRD Table指针寄存器传入配置好的PRD Table的地址,设置读写控制位,清除中断和错误位。
3) 设置读写命令,包括读写的驱动器,逻辑地址等(这里基本上和PIO类似)。
4) 设置总线主控IDE命令寄存器的开始/停止位为1,控制器开始执行DMA操作。
5) 控制器DMA操作结束,IDE设备发起中断,收到中断后,设置开始/停止位为0(我们省略了读取状态寄存器来查看操作是否成功的步骤。)

如果只是从IDE方面来看,代码没有复杂多少,可惜的是他还需要配合其他计算机硬件,所以实际要用上的代码要比PIO多上了不少。最后还是给大家推荐一些深入理解DMA的资料吧。
ISA DMA:http://wiki.osdev.org/ISA_DMA
PCI DMA:http://wiki.osdev.org/ATA/ATAPI_using_DMA

MiniKernel

使用PCI IDE Controller读写硬盘 – 1

PIO_hd04

上一篇文章中提到了一些IDE基础的知识,并且知道了如何判断IDE的类型。接下来介绍IDE最基本的IO方法——PIO。

PIO是Programmed input/output的缩写,下面是一段wiki上对PIO的介绍:
“可编程输入输出(英语:PIO)是CPU与外围设备(如网卡、硬盘等)传输数据的一种方法。当 CPU 上执行的软件程序使用 I/O 地址空间来与输入/输出设备(I/O 设备)进行数据传输时,系统即进行了 PIO. 这和直接内存存取(DMA)恰好相反。

在 PC 上最常见的使用 PIO 的例子是 ATA 接口,但 ATA 接口也可以在 DMA 模式下工作。 PC 上的许多比较古老的设备也使用 PIO, 如串行端口、并行端口(在不使用 ECP 模式时)、PS/2 接口、MIDI 接口、内部时钟以及一些古老的网卡。”

实际上,在DMA出现之前,PIO是硬盘唯一的数据传输的方式。就算是现在,ATA的部分命令还必须使用PIO的方式获得数据,例如DEVICE IDENTIFY。PIO传输数据的思想简单直接,例如从硬盘都数据,只需要在硬盘准备好了之后,不断的读取特定端口就能将数据读出来了。例如 in eax, dx(dx里是数据端口号),这样每次就传输4个字节,也就是说如果需要传输512(一个扇区)的数据,需要128次IO。这样一方面数据传输的效率难以提高,另一方面还占用了CPU时间。所以被DMA淘汰也是有道理的。然而,他也有自身的优势,那就是编程起来简单方便。不像DMA那样,需要配置中断和其他一些事情。简单的对IDE下命令就可以达到数据传输的目的了。所以这也是我们入门的很好的切入口。

最后,在开始堆代码之前,我们必须了解硬盘的两种寻址模式:CHS(cylinders-heads-sectors,磁柱-磁头-扇区)寻址模式和LBA(Logical Block Address, 逻辑区块地址)寻址模式。
CHS寻址模式,区块必须以硬盘上某个磁柱、磁头、扇区的硬件位置所合成的地址来指定。
LBA寻址模式从0开始编号来定位区块,第一区块LBA=0,第二区块LBA=1,依此类推。

前者的描述更偏向物理,理解起来需要转换,而后者更偏向思维逻辑,理解起来直接了当。既然LBA模式简单容易理解,所以下面的文章和代码所采用的寻址模式就默认是LBA28。所谓LBA28其实是LBA模式中的子模式,它可以寻址到128GB,与之对应的是LBA48,它的寻址范围可以达到128PB,这个对我们来说没啥意义,所以还是选用LBA28。另外CHS模式还需要解释硬盘机械方面的知识,前提太多不利于学习,就暂时搁下吧。

好了,介绍理论的知识不是这篇文章的目的,就让我们一边堆代码,一边讲解这些理论知识吧。下面就是一段读取硬盘数据的asm代码。

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
35
36
37
38
39
40
41
    mov dx, 3f6h    ; 1.设置nIEN
mov al, 2h
out dx, al

mov dx, 1f1h ; 2.设置FEATURES为0
mov al, 0
out dx, al

mov dx, 1f2h ; 3.设置读取的扇区数量,这里指定为1
mov al, 1
out dx, al

mov dx, 1f3h ; 4.设置读取地址的低八位
mov al, 11h
out dx, al

mov dx, 1f4h ; 5.设置读取地址的中八位
mov al, 22h
out dx, al

mov dx, 1f5h ; 6.设置读取地址的高八位
mov al, 33h
out dx, al

mov dx, 1f6h ; 7.设置LBA模式,目标驱动器,和地址的最高4位
mov al, 44h ; 其中第6位(40h)是设置LBA模式,第4位设置主从驱动器,0为主驱动器
out dx, al ; 后四位就是LBA最高位了,这里是4h,也就是说读取的地址是04332211h

mov dx, 1f7h ; 8.设置读取扇区的命令20h
mov al, 20h
out dx, al
pri_stat:
in al, dx ; 9.轮询状态寄存器,第3位(8)如果是设置状态,表明可以进行数据传输了。
test al, 8
jz pri_stat

mov ecx, 512/4 ; IO 128次!
mov edi, offset buffer ; 设置buffer地址到edi
mov dx, 1f0h
rep insw ; 10.循环128次从数据寄存器读取1个扇区的数据

上面的代码主要分为以下这几步:
1.我们现在不需要IRQs,所以我们这里要禁用它,以免发生不必要的问题。这里,我们设置CBR(Control Block Register)的第1位,也叫nIEN位,只要它处于设置的状态,那么IRQs就不会触发。
2.设置FEATURES寄存器
3.设置扇区数寄存器
4.设置LBA低8位
5.设置LBA中8位
6.设置LBA中高8位
7.设置LBA最高4位,以及驱动器,指明使用LBA模式
8.设置读扇区命令
9.轮询等待完成状态
10.循环128次读取1个扇区的数据(128*4=512 bytes)

再看看写扇区有哪些不同呢?没错,只有最后几条指令有细微的差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
    mov dx, 1f7h
mov al, 30h ; 这里命令改为30h,写扇区
out dx, al
pri_stat:
in al,dx
test al,8
jz pri_stat

mov ecx, 512/4
mov esi, offset buffer ; 设置buffer地址到esi
mov dx, 1f0h
rep outsd ; 这里指令改为outsd

看起来还是很简单的吧!不过这也是当然的,因为我们没有做任何的排错和检验处理。不过这样的代码才是初学者最喜欢看到的吧,同样对于做操作系统实验也没多大问题。

最后,如果对CHS以及LBA和CHS的转换关系感兴趣,推荐翻阅wiki:
http://en.wikipedia.org/wiki/Cylinder-head-sector
http://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion

在下一篇的文章中,会介绍一些用PCI DMA的方式读写硬盘的知识。

MiniKernel

使用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