Chinaunix首页 | 论坛 | 博客
  • 博客访问: 544497
  • 博文数量: 252
  • 博客积分: 6057
  • 博客等级: 准将
  • 技术积分: 1635
  • 用 户 组: 普通用户
  • 注册时间: 2009-12-21 10:17
文章分类

全部博文(252)

文章存档

2013年(1)

2012年(1)

2011年(32)

2010年(212)

2009年(6)

分类:

2010-06-01 14:33:41

关键字:文件系统驱动编程,端口读写

作者:wskjuf    更新:2008-10-06 22:14:58    浏览:7539

文件系统驱动编程基础篇之七——端口读写

一、前略

    本系列文章为业余编程爱好者而写,仅仅作为初学者的一个借鉴,真正的精华存在于参考资料*中。知识 的积累将经历从薄到厚,再从厚到薄的反复过程,为了打下牢固的基础,请读者务必在阅读本文的基础上花费必要的时间完成参考资料。

笔者的实践环境为:
硬件:P35 Motherboard & ICH9 chip,Pentium Dual Cpu E2160 1.8g, DDR2 1g
软件:Windows XP2、VS 2005、Visual AssistX、DriverStudio 3.2、MICROSOFT.WINDOWS.SERVER.V2003.IFS.DDK、Windbg 6.8.0004.0,请安装好用于调试的虚拟机并配置好调试环境。

参考资料*:
1.《Programming the Microsoft Windows driver model》第一版(当前阶段主要阅读资料,在先前的基础上,本次需要完成前八章)
2.《Windows NT File System Internals - A Developers Guide》(资料1理解后可阅读,为后续章节做准备)
3.《OSR White Papers》
4、《Intel 64 and IA-32 Architectures Software Developer s Manual Volume 3A/B System Programming Guide》
5.WinXXX部分源代码…
6.《Kernel Debugging with WinDbg》(随WinDbg软件附带的文档)

阅读基础:了解计算机结构体系、掌握用户模式下常用API调用、了解常用汇编指令。请从现在开始的一年时间里阅读50万字以上的驱动编程外文资料,彻底突 破外文关。

本章目的:初步掌握硬件驱动编程的基础知识,学习使用Windbg调试。初次阅读代码量在一万行以上的驱动程序。


二、问题的提出

    xp下直接读写端口会发生什么事情呢?一个简单的测试程序如下:

int main(int argc, char* argv[])
{
    char result;
    asm {
    mov al, 0x10
    out 0x70, al
    in al, 0x71
    mov result, al
    }
    printf("%X", result);
    return 0;
}


运行后发现程序弹出了异常,无法执行out指令,给出的提示为:

    这是早已预料的结果。笔者搜索了一下,找到了《Direct Port IO and Windows NT》,欣欣然之下禁不住想让大家知道原来“不需要”写驱动也可以访问端口了。它究竟是何方神圣?原来这是一种称之为iopm的方法,它涉及了 CPU硬件理论里关于TS段的基础知识:



    通过修改I/O映射表,用户模式下的进程就具有了存取端口的权限。问题是如何修改I/O映射表?阅读了porttalk和winio的源代码后, 发现所谓的iopm还是需要通过驱动代码修改映射表,此外你还需要修改当前进程的映射表偏移地址。当我们费力的写好近千行代码,换来的好处就是真的可以在 用户模式下以较快的速度存取端口了。欣欣然的你不妨用文初给出的测试程序来检查一下成果:

    如果把驱动编程想得这么的简单,那你就有些过于乐观了。现在尝试另一个端口,比如串口的0x3f8,看看有什么奇怪的事情发生?这次读出了 0xff,那么0x3f9呢,还是0xff,一直读完串口的所有端口,结果都是0xff。假如我们首先用CreateFile来打开串口,怎么读出的值不 再是0xff了,如果关闭了串口再来读一读,结果又是0xff了。Google或baidu后,发现串口原理的资料很少,换个思路来查,查询串口的主要芯 片——从8250一直搜索到16550A,这下果然搜出了有价值的资料,如《Interfacing the Serial RS232 Port》,《串行输入输出接口》,《常用的输入输出接口芯片》,《UART 内部寄存器对应端口及用途》等。但这些资料只说明了一个问题——很久很久以前,在DOS下是如何发送串口指令的——却还是没有解决Windows下的问 题。
    让我们再换一下思路。从DevView里看到串口驱动的基本情况,原来串口的PDO是ACPI。

    阅读《Advanced Configuration and Power Interface Specification》,可知ACPI是微软和几个厂家制定的协议,不由得想起使用WINPE启动的时候选择了ACPI,结果机子一次次的重启,本 杂牌机无论从外观还是认证标签上看极象兼容ACPI,但假货毕竟是假货。折腾了半天后,因我们的目的不在于如何写ACPI驱动,故这个问题的答案即使和它 有关,也请暂时绕过(实践会证明,很多问题都是冤家路窄,避是避不过的^_^。对ACPI调用IoCallDriver后,激活了串口)。

三、串口代码之实践

    本篇最终目的是阅读并调试串口程序,因笔者使用的DDK版本是3790的,源代码位在WINDDK\3790\src\kernel \serial下。首先我们要做的是完善串口的调试输出信息函数,修改位于log.c下的SerialDbgPrintEx如下:

ULONG
SerialDbgPrintEx(IN ULONG Level, PCHAR Format, ...) // 级别,格式,不定的参数
{
    va_list arglist;
    ULONG rval;
    ULONG Mask = 0;
    ULONG cb;
    UCHAR buffer[SERIAL_DBGPRINT_BUFSIZE];

    RTL_QUERY_REGISTRY_TABLE paramTable[4];
    ULONG zero = 0;

    if (KeGetCurrentIrql() == PASSIVE_LEVEL)
    {
        RtlZeroMemory (¶mTable[0], sizeof(paramTable));
        paramTable[0].Flags = RTL_QUERY_REGISTRY_DIRECT;
        paramTable[0].Name = L"SerialDebugLevelBegin";
        paramTable[0].EntryContext = &SerialDebugLevel; // 保存查询结果
        paramTable[0].DefaultType = REG_DWORD;
        paramTable[0].DefaultData = &zero;
        paramTable[0].DefaultLength = sizeof(ULONG);

        paramTable[1].Flags = RTL_QUERY_REGISTRY_DIRECT;
        paramTable[1].Name = L"SerialDebugLevelEnd";
        paramTable[1].EntryContext = &SerialDebugLevelEnd; // 保存查询结果
        paramTable[1].DefaultType = REG_DWORD;
        paramTable[1].DefaultData = &zero;
        paramTable[1].DefaultLength = sizeof(ULONG);

        paramTable[2].Flags = RTL_QUERY_REGISTRY_DIRECT;
        paramTable[2].Name = L"SerialDebugLevelCloseHigh";
        paramTable[2].EntryContext = &SerialDebugLevelCloseHigh; // 保存查询结果
        paramTable[2].DefaultType = REG_DWORD;
        paramTable[2].DefaultData = &zero;
        paramTable[2].DefaultLength = sizeof(ULONG);

        RtlQueryRegistryValues( RTL_REGISTRY_ABSOLUTE | RTL_REGISTRY_OPTIONAL,
            SerialGlobals.RegistryPath.Buffer, // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Serial
            ¶mTable[0], // 以全0的结尾表结束,这样一次就可以查询多个值了
            NULL,
            NULL);
        Mask = Level;
    }
    else if (SerialDebugLevelCloseHigh == 1) // 不显示高irql信息
    {
        return STATUS_SUCCESS;
    }

    if (Mask < SerialDebugLevel || Mask > SerialDebugLevelEnd) {
        return STATUS_SUCCESS;
    }

    va_start(arglist, Format); // arglist越过了格式串,指向了参数

    cb = _vsnprintf(buffer, sizeof(buffer), Format, arglist);

    if (cb == -1) { // 串长于buffer
        buffer[sizeof(buffer) - 2] = '\n';
    }

    DbgPrint("IRQL %d, SERIAL: %s", KeGetCurrentIrql(), buffer);
    //rval = vDbgPrintEx(DPFLTR_SERIAL_ID, Level, Format, arglist);

    va_end(arglist);

    rval = STATUS_SUCCESS;

    return rval;
}

    往注册表里添加 SerialDebugLevelBegin,SerialDebugLevelEnd,SerialDebugLevelCloseHigh三键,我们 可以更灵活的动态控制调试信息。

    编译好Checked模式的串口驱动程序后,接下来需要替换掉系统自带的串口驱动WINDOWS\system32\drivers\ serial.sys(大小为59KB)。希望你首先按照自己的思路来尝试替换,如果失败了再来尝试如下的方法:

  1. 关闭系统还原
  2. 改名WINDOWS\system32\dllcache\serial.sys
  3. 将WINDOWS\system32\CatRoot目录改名为其他名字
  4. 将你编译好的checked模式的串口驱动程序覆盖WINDOWS\system32\drivers\serial.sys
  5. 在硬件管理器里删除掉串口驱动,再重新扫描硬件变更,重新安装驱动时可能出现“是否替换…”的提示,选择否,直到你发现驱动目录下的串口驱动 程序确实是你自己编译过的程序
  6. 运行Dbgview后在硬件管理器里停止串口驱动,再启动驱动,此时可以看见在Dbgview里看见调试信息(前提是注册表已设好上述三键的 值)

    为了方便阅读代码,简单说明一下串口寄存器。端口0x3f8-0x3fe 用于微机上COM1 串行口,0x2f8-0x2fe 对应COM2 端口。DLAB(Divisor Latch Access Bit)是除数锁存访问位,是指线路控制寄存器的最高位7。

表 UART 内部寄存器对应端口及用途

序号

端口

读/写

条件

用途

0

0x3f8 (0x2f8)

DLAB=0

发送保持寄存器。含有将发送的字符。 TRANSMIT_HOLDING_REGISTER

 

 

DLAB=0

接收缓存寄存器。含有收到的字符。 RECEIVE_BUFFER_REGISTER

 

 

读/写

DLAB=1

波特率因子低字节(LSB)。DIVISOR_LATCH_LSB

1

0x3f9 (0x2f9)

读/写

DLAB=1

波特率因子高字节(MSB)。DIVISOR_LATCH_MSB

 

 

读/写

DLAB=0

中断允许寄存器。INTERRUPT_ENABLE_REGISTER

 

 

 

 

位7-4 全0 保留不用;

 

 

 

 

位3=1 modem 状态中断允许;

 

 

 

 

位2=1 接收器线路状态中断允许;

 

 

 

 

位1=1 发送保持寄存器空中断允许;

 

 

 

 

位0=1 已接收到数据中断允许。

2

0x3fa (0x2fa)

 

中断标识寄存器。中断处理程序用以判断此次中断是4种中的那一种。 INTERRUPT_IDENT_REGISTER

 

 

 

 

位7-3 全0(8250不用,16550:7-6=11 允许fifo, =00 不允许fifo);

 

 

 

 

位2-1 确定中断的优先级;

 

 

 

 

= 11 接收状态有错中断,优先级最高;

 

 

 

 

= 10 已接收到数据中断,优先级第2;

 

 

 

 

= 01 发送保持寄存器空中断,优先级第3;

 

 

 

 

= 00 modem 状态改变中断,优先级第4。

 

 

 

 

位0=0 有待处理中断;=1 无中断。

3

0x3fb (0x2fb)

 

线路控制寄存器。LINE_CONTROL_REGISTER

 

 

 

 

位7=1 除数锁存访问位(DLAB)。 =0 访问发送保持寄存器、接收缓存寄存器器或中断允许寄存器;

 

 

 

 

位6=1 强迫SOUT送出空闲状态;0 禁止间断;

 

 

 

 

保持奇偶位 位5=1 偶校验时,校验位=0 奇校验时,校验位=1;0 无数

 

 

 

 

位4,3=11 偶校验;=01 奇校验;x0 不加校验位

 

 

 

 

位2=1 (5位数据位时)1.5位停止位,(6,7,8位数据位时)2位停止位;=0 1位停止位;

 

 

 

 

位1-0 数据位长度:= 00 5位数据位;= 01 6位数据位;= 10 7位数据位;= 11 8位数据位。

4

0x3fc (0x2fc)

 

modem 控制寄存器。MODEM_CONTROL_REGISTER

 

 

 

 

位7-5 全0 保留;

 

 

 

 

位4=1 芯片处于循环反馈诊断操作模式;(自测试循环)SERIAL_MCR_LOOP

 

 

 

 

位3=1 辅助用户指定输出2,允许INTRPT 到系统;SERIAL_MCR_OUT2

 

 

 

 

位2=1 辅助用户指定输出1,PC 机未用;SERIAL_MCR_OUT1

 

 

 

 

位1=1 使请求发送RTS 有效;SERIAL_MCR_RTS

 

 

 

 

位0=1 使数据终端就绪DTR 有效。SERIAL_MCR_DTR

5

0x3fd (0x2fd)

 

线路状态寄存器。LINE_STATUS_REGISTER

 

 

 

 

位7=0 保留;

 

 

 

 

位6=1 发送移位寄存器为空;

 

 

 

 

位5=1 发送保持寄存器为空,可以取字符发送;

 

 

 

 

位4=1 接收到满足间断条件的位序列;

 

 

 

 

位3=1 帧格式错误;

 

 

 

 

位2=1 奇偶校验错误;

 

 

 

 

位1=1 超越覆盖错误;

 

 

 

 

位0=1 接收器数据准备好,系统可读取。

6

0x3fe (0x2fe)

 

modem 状态寄存器。δ 表示信号发生变化。MODEM_STATUS_REGISTER

 

 

 

 

位7=1 载波检测(CD)有效;

 

 

 

 

位6=1 响铃指示(RI)有效;

 

 

 

 

位5=1 数据设备就绪(DSR)有效;

 

 

 

 

位4=1 清除发送(CTS)有效;

 

 

 

 

位3=1 检测到δ 载波;

 

 

 

 

位2=1 检测到响铃信号边沿;

 

 

 

 

位1=1 δ 数据设备就绪(DSR);

 

 

 

 

位0=1 δ 清除发送(CTS)。

#define RECEIVE_BUFFER_REGISTER??? ((ULONG)((0x00)*SERIAL_REGISTER_STRIDE))
// 本文转自 C++Builder研究 - article.asp?i=1060&d=fu044r
#define TRANSMIT_HOLDING_REGISTER? ((ULONG)((0x00)*SERIAL_REGISTER_STRIDE))
#define INTERRUPT_ENABLE_REGISTER? ((ULONG)((0x01)*SERIAL_REGISTER_STRIDE))
#define INTERRUPT_IDENT_REGISTER?? ((ULONG)((0x02)*SERIAL_REGISTER_STRIDE))
#define FIFO_CONTROL_REGISTER????? ((ULONG)((0x02)*SERIAL_REGISTER_STRIDE))
#define LINE_CONTROL_REGISTER????? ((ULONG)((0x03)*SERIAL_REGISTER_STRIDE))
#define MODEM_CONTROL_REGISTER???? ((ULONG)((0x04)*SERIAL_REGISTER_STRIDE))
#define LINE_STATUS_REGISTER?????? ((ULONG)((0x05)*SERIAL_REGISTER_STRIDE))
#define MODEM_STATUS_REGISTER????? ((ULONG)((0x06)*SERIAL_REGISTER_STRIDE))

    前几章中涉及的代码量不过千行左右,此次阅读的个别子例程代码量已达到上千行,所以要特别注意阅读的技巧。首先大略了解各文件的基本功能, 从DriverEntry起,分步阅读各个派遣例程,适时记录流程中涉及的关键函数,至少读懂全部代码的45%并攻克本文提出的问题。
    如果你已经开始厌烦没完没了的书写重复的代码,那么你就应该有所理解为何微软在KMDF模型下提供现成的PNP和电源管理代码的原因了。

四、结语

    如果不理解何为“浮光掠影”,阅读了本文后就应该有原来如此的感觉了。
我们的目的是文件系统驱动编程,为何要涉及硬件驱动编程的学习?原因很简单,硬件驱动编程和文件系统编程在出现分叉点之前的知识是通用的,必须扎实的掌 握。我们在篇二时已经实现了一次三级跳,今天这次跳跃将使你的水平更加接近文件系统编程所需要的层次。
    至此驱动编程的基础知识都已有所接触,如果你对资料1的前八章知识仍感到困难,不妨放松心情,休息几天,找出原因后继续前进。我希望大家完成本篇 的时候,时间应控制在三个半月以下(应该算是严格的要求,但我相信大家的智慧)。接下来我们将使用三个月的时间学习文件系统的基本知识,并投入六个月以上 的时间进行实践。希望一年之后,您有脱胎换骨的感觉。
阅读(1673) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~