Kernel-win32的系统调用机制

毛德操

正如许多网友所言,要在Linux内核中实现Windows系统调用(或别的系统调用),最简单的办法莫过于把
这些系统调用“搭载”在Linux系统调用上。具体又有几种不同的方法:
1. 为Linux系统调用ioctl()增加一些“命令码”,每个新的命令码都代表着一个Windows系统调用。
2. 为Linux增加一个新的系统调用、例如win32_syscall()、作为总的入口和载体,然后定义一些类似
于ioctl()中所用那样的命令码。
3. 在Linux系统中定义一种虚拟的特殊文件,然后把Windows系统调用搭载在某个文件操作的系统调用
上,例如ioctl()、read()等等都可以用于这个目的。作为一种特例,在/proc下面增加一个节点,就可
以用于这个目的。又如socket也可以看作是这样的特殊文件。
4. 其它。例如也可以采取类似于“远程过程调用”、即RPC的形式,但是让“服务端”成为内核线程,
或者直接在调用者的上下文中执行(这实际上是第3种方法的变种)。
其中又以第1、2两种方法更为简单易行。事实上Kernel-win32正是这样做的。

Kernel-win32原先在这方面提供两种选项。一种是利用Linux的ioctl()系统调用;另一种是为Linux增添
一个win32()系统调用,然后在这个新添系统调用的内部采用类似于ioctl()那样的实现。但是在后来的
版本中已经放弃了采用ioctl()的选项(在相应的代码中加上了“#if 0”),所以现在已经只采用上述的

第二种方法,即为Linux增加一个系统调用作为载体。下面我们看它的代码。
首先,Kernel_win32为Linux定义了一个新的系统调用号:

/* Linux Win32 emulation syscall */
#define __NR_win32 249

新的调用号__NR_win32定义为249。这个调用号在当时无疑是空闲的,但是在Linux内核的2.6.14版中分
配使用的系统调用号已经达到了288,而调用号为249的系统调用是io_cancel()。所以如果要在2.6.14版
内核上使用Kernel-win32肯定得要修改这个调用号的定义。其实,这也反映出此种方法的缺点:只要是
没有被正式纳入Lunux内核代码的系统调用号,都是靠不住的。此外,注释中说这是用来实现(Windows)
系统调用“仿真(emulation)”的,但是我认为这只能说是“模拟(simulation)”,因为只是逻辑上相同
、而形式上是不同的。
这个Linux系统调用只是个载体,而实际的Windows系统调用号(更确切地说是Kernel_win32系统调用号)
,则另有定义:

/* Win32 system call numbers */
typedef enum {
WINESERVER_INITIALISE_WIN32,
WINESERVER_UNINITIALISE_WIN32,
WINESERVER_CLOSE_HANDLE,
WINESERVER_WAIT_FOR_MULTIPLE_OBJECTS,
WINESERVER_CREATE_MUTEX,
……
WINESERVER_MAP_VIEW_OF_FILE,
WINESERVER_UNMAP_VIEW_OF_FILE,
WINESERVER__LAST
} WineSyscallNum;

一共是30个调用号(最后的WINESERVER__LAST并非有效的调用号)。注意这些调用号跟真正的Windows系统
调用号是完全不同的,例如NtCloseHandle()的调用号是24,而在这里是2。而且,所定义的许多系统调
用在Windows中并没有对应物,例如开头两个就是这样。至于Windows中有、而在这里没有定义的系统调
用,那就更多了(Win2k有248个系统调用)。所以这实际上不能说是Windows系统调用。代码中称为
WineSyscallNum,意思大概是把Wine的一些RPC函数转化成了系统调用。
此外,作为可安装模块的Kernel-win32还要在初始化时“登记”用来实现这个系统调用的函数。

static int __init wineserver_init_module(void)
{
#ifdef USE_WIN32SYSCALL
int tmp;
#endif
……
#ifdef USE_WIN32SYSCALL
/* register the syscall */
tmp = register_win32_syscall(__NR_win32,win32syscall, &wineserver_ornament_ops);
if (tmp<0) {
remove_proc_entry("wineserver",NULL);
return tmp;
}
#endifreturn 0;
} /* end wineserver_init_module() */

要登记的函数是win32syscall(),而指针&wineserver_ornament_ops是要传递给这个函数的参数。我们
往下看register_win32_syscall()的代码。

[wineserver_init_module() > register_win32_syscall()]int register_win32_syscall(int syscall, win32syscall_func func,
const struct task_ornament_operations *ornament_type)
{
……
if (!w32handler) {
if (cmpxchg(&sys_call_table[syscall],
(long)sys_ni_syscall,
(long)sys_win32
)==(long)sys_ni_syscall
) {
/* we installed it successfully */
w32syscall = syscall;
w32handler = func;
w32ornament_type = ornament_type;
}
ret = 0;
}
……
return ret;
} /* end register_win32_syscall() */

显然,实际填写到系统调用(跳转)表sys_call_table[ ]中的函数指针是sys_win32()。

应用软件在需要进行Windows系统调用(更确切地说是Kernel_win32系统调用)时通过一个库函数win32()
实施调用。例如要调用CreateFile()时就这样调用:
int i = win32(WINESERVER_CREATE_FILE, &args);
参数WINESERVER_CREATE_FILE就是系统调用号,或称“命令码”。而真正用于CreateFile()的参数,则
都组装在一个数据结构中,&args就是这个数据结构的地址。
库函数win32()的代码很简单:

static __inline__ int win32(int cmd, void *args)
{
#ifdef USE_WIN32SYSCALL
return syscall(__NR_win32, cmd, args);
#else
#error must use Win32 Syscall
#endif
}

这里的syscall()是C库中的一个函数,其代码在glibc的一个源文件syscall.s中,有兴趣的读者可以自
行阅读。其作用则不言自明:__NR_win32是Linux系统调用号;cmd是命令码、即Kernel_win32系统调用
号;args是指向参数结构的指针。后面两项都是对于Linux系统调用win32()的参数。
进入内核以后,CPU根据系统调用号和内核中的系统调用(跳转)表进入内核函数sys_win32():

asmlinkage int sys_win32(unsigned int cmd, void *args)
{
struct task_ornament *orn;
win32syscall_func fnx;
int error;
……
fnx = w32handler;
……
/* find the ornament on the current task (does ornget if successful) */
orn = task_ornament_find(current, w32ornament_type);
……
/* invoke the handler */
error = fnx(orn,cmd,args);
……
return error;
} /* end sys_win32() */

从代码中可以看出,这个函数只是中转,真正的目的是要通过前面登记的函数指针w32handler调用
win32syscall()。那么为什么不直接把win32syscall()放在系统调用表中,从而直接进入win32syscall
()呢?比较一下两个函数的调用参数就可以知道,后者要求以指向当前线程的task_ornament结构指针作
为参数,而内核根据系统调用表中的函数指针进行调用时是不能带额外参数的。之所以需要这个指针,
显然是因为有关Windows线程的补充信息都在这个数据结构中,或者从这个数据结构开始才能找到。
为了要得到当前线程的这个task_ornament结构指针,这里通过task_ornament_find()在当前
task_struct的ornament队列中寻找。这里的指针w32ornament_type 指向数据结构
wineserver_ornament_ops,这是前面登记系统调用函数指针时设置好的。

[sys_win32() > task_ornament_find()]struct task_ornament *task_ornament_find(struct task_struct *tsk,
struct task_ornament_operations *type)
{
struct task_ornament *orn;
struct list_head *ptr;read_lock(&tsk->alloc_lock);
for (ptr=tsk->ornaments.next; ptr!=&tsk->ornaments; ptr=ptr->next) {
orn = list_entry(ptr,struct task_ornament,to_list);
if (orn->to_ops==type)
goto found;
}read_unlock(&tsk->alloc_lock);
return NULL;
found:
ornget(orn);
read_unlock(&tsk->alloc_lock);
return orn;
} /* end task_ornament_find() */

这段程序扫描给定task_struct结构的ornament队列,从中寻找类型为type、实际上是指向
wineserver_ornament_ops的task_ornament数据结构。其实,正如在“对象管理”那篇漫谈所述,这个
队列中一般只有一个数据结构,而且其类型也正是wineserver_ornament_ops。所以是否真的需要如此大
动干戈是值得推敲的。
找到了这个补充性的数据结构,就可以调用win32syscall()了。

[sys_win32() > win32syscall()]#ifdef USE_WIN32SYSCALL
int win32syscall(struct task_ornament *orn, unsigned int syscall, void *uargs)
{
struct WineThread *thread;
void *args;
int err;
……
if (copy_from_user(args, uargs, ioctl_cmds[syscall].ic_argsize)) {
err = -EFAULT;
goto cleanup;
}/* invoke the syscall handler */
if (orn)
thread = orn_entry(orn,struct WineThread,wt_ornament);
else
thread = NULL;
……
err = ioctl_cmds[syscall].ic_handler(thread,args,uargs);
……
return err;
} /* end win32syscall() */
#endif

先从用户空间把实际的调用参数(组装在一个数据结构中)复制到系统空间的一个缓冲区。下面的
orn_entry是个宏操作,目的是把task_ornament结构指针换算成WineThread结构指针。因为前者是后者
内部的一个成分,所以可以进行换算。
关键的操作就是根据Kernel-win32系统调用号syscall从系统调用表ioctl_cmds[ ]中取得目标函数指针
并加以调用。这个数组之所以叫ioctl_cmds,是因为原先Kernel-win32是通过ioctl()进行调用的。

static const struct _ioctl_cmd ioctl_cmds[] =
{
_WIN32(InitialiseWin32),
_WIN32(UninitialiseWin32),
_WIN32(CloseHandle),
_……
_WIN32(CreateFileA),
_WIN32(ReadFile),
_WIN32(WriteFile),
……
_WIN32(CreateFileMappingA),
_WIN32(MapViewOfFile),
_WIN32(UnmapViewOfFile),
{ 0, NULL }
};

这里宏操作_WIN32的定义为:

#define _WIN32(X) { sizeof(struct Wioc##X), X }

以数组中的元素_WIN32(CreateFileA)为例,经过编译以后就成为:

{sizeof(struct WiocCreateFileA), CreateFileA}

所以前面引用的ioctl_cmds[syscall].ic_argsize和ioctl_cmds[syscall].ic_handler分别为参数结构
的大小和函数指针。再往下的事就不用说了。

把Windows系统调用(或Kernel-win32系统调用)搭载在Linux系统调用上的做法简单易行,但是也有缺点

首先是降低了效率。从上述的过程一步一步下来,可以看出系统的开销还是不小的。这里面有的是因为
Kernel-win32具体的设计所引起,实际上还可优化;有的却是这种方法所固有的,这跟CPU的“间接寻址
”与“直接寻址”的区别有些相似。这一点开销,对于本身较大、较费时的系统调用而言固然可以忽略
不计;但是对于本身较小、特别是需要频繁调用的系统调用而言却是不可忽略的了。
更重要的是,由于是搭载在Linux系统调用上,返回用户空间时也必定跟Linux系统调用走同一条路线。
然而在这方面两个系统是有区别的。在返回的过程中,Linux要检查是否有Signal,如果有就要在用户空
间加以执行(类似于对用户空间程序的中断),而Windows则要检查和处理APC。这二者原理相似,但是具
体的实现还是有差别的(至少有待研究)。显然,最好是能够各走各的路,否则对于要达到高度兼容的目
的是不利的。
另一方面,这种方法对于用户空间DLL的实现、特别是ntdll.dll的实现有了特殊要求。所以这种方法只
能说是“模拟”而不是“仿真”。为了达到高度兼容的目标,在测试时最好能够把我们的全套DLL、包括
ntdll、安装到Windows上去,再运行Windows应用软件,看其表现和效果是否与使用“原装”DLL时相同
。反过来,也最好能把Windows的原装DLL和应用软件安装到Linux上(如果这样做不构成侵犯版权的话),
以检验Linux兼容内核对Windows系统调用的支持是否正确与完整。这就要求二者采用相同的机制与手段
,例如都采用int 0x2e,发生0x2e自陷时有相同的堆栈内容,采用相同的系统调用号,等等。而且,有
些特殊的应用软件甚至可能绕过Win32 API,而直接通过int 0x2e进行系统调用,对于这样的应用显然就
无法在DLL中拦截其系统调用并加以转换。更何况Windows的系统调用还不完全限于int 0x2e这一种手段
,还有0x2b、0x2c、0x2d也是特殊的系统调用手段,而且那些调用更有可能绕过Win32 API。当然,那些
调用的实现是将来的事,甚至可能不会去实现,但作为设计方案也应该加以考虑、或留下余地。
所以,对于兼容内核,我们应该考虑采用int 0x2e作为系统调用的手段,并实现一套跟Windows尽可能一
致的机制。具体地,就是要把ReactOS的系统调用机制与Linux的系统调用机制揉合在一起,使其在系统
空间与用户空间的分界线上呈现出跟Windows尽量一致、甚至完全一致的特性。其实这也并不像有些人想
像的那么难,实际上我们已经调通了这样的一个雏型,春节之后整理一下就可以把源码公开出来。
========

理解Windows内核模式与用户模式

http://blog.csdn.net/wzy198852/article/details/32335371

1、基础

运行 Windows 的计算机中的处理器有两个不同模式:“用户模式”和“内核模式”。根据处理器上运行

的代码的类型,处理器在两个模式之间切换。应用程序在用户模式下运行,核心操作系统组件在内核模

式下运行。多个驱动程序在内核模式下运行,但某些驱动程序在用户模式下运行。

当启动用户模式的应用程序时,Windows 会为该应用程序创建“进程”。进程为应用程序提供专用的“

虚拟地址空间”和专用的“句柄表格”。由于应用程序的虚拟地址空间为专用空间,一个应用程序无法

更改属于其他应用程序的数据。每个应用程序都孤立运行,如果一个应用程序损坏,则损坏会限制到该

应用程序。其他应用程序和操作系统不会受该损坏的影响。

用户模式应用程序的虚拟地址空间除了为专用空间以外,还会受到限制。在用户模式下运行的处理器无

法访问为该操作系统保留的虚拟地址。限制用户模式应用程序的虚拟地址空间可防止应用程序更改并且

可能损坏关键的操作系统数据。

在内核模式下运行的所有代码都共享单个虚拟地址空间。这表示内核模式驱动程序未从其他驱动程序和

操作系统自身独立开来。如果内核模式驱动程序意外写入错误的虚拟地址,则属于操作系统或其他驱动

程序的数据可能会受到损坏。如果内核模式驱动程序损坏,则整个操作系统会损坏。

此图说明了用户模式组件与内核模式组件之间的通信。

框图:用户模式组件和内核模式组件

2、内核层次架构

下面是内核的层次划分:

硬件抽象层(HardwareAbstraction Layer) (HAL) (hal.dll)
最底层隔离硬件的,底层的第三方驱动程序就运行在这层。

内核(Kernel)
实现操作系统的一些底层服务,比如线程调度,多处理器的同步,中断/异常处理等。

执行体(Executive)(ntoskrnl.exe)
实现基本的操作系统服务,比如基本的线程进程管理,内存管理, IO及进程间通讯等。

窗口图形子系统(Windows Graphics Subsystem)
由win32K.sys在内核层实现,用户界面相关都依赖该层,User32.dll的大部分功能都由该层实现。

用户层关键进程

Windows系统在用户层有几个关键的系统进程:

Smss.exe(session manager Subsystem)
关于Session的概念可以参考我的这篇Sessions, Window Stationsand Desktops,在操作系统启动时会

创建一个不与任何Session关联的Smss.exe管理者实例,然后当有用户登录时它会为每个Sessin拷贝一份

与之关联的Smss.exe实例,然后由该关联的Smss.exe实例启动winlogon.exe和csrss.exe.

WinLogon.exe
该进程管理用户的登录和注销,我们按Ctrl+Alt+Del出现的界面和登录后出现的桌面窗口都是由它启动

的。

Csrss.exe( Client/Server Runtime Subsystem)
我们可以看到我们的桌面窗口(GetDesktopWindow)是由该进程创建的,该进程主要负责Win32子系统的用

户模式部分(内核模式部分由win32k.sys实现)。

Lsass.exe(Local Security Authority Subsystem)
WinLogon.exe通过该进程验证用户登录,登录后产生安全访问令牌对象,通过该令牌创建Explorer.exe,

我们其他用户进程都由Explorer.exe启动,并且继承了该令牌权限。

Services.exe
该进程简称为SCM(NT Service Control Manager),该进程负责启动用户态一些特殊进程,也就是我们通

常所说的服务程序。

3、用户模式调用内核模式的方式

4、内核模式调用用户模式

可以通过IOCTL的上下文传递,也可以通过APC (Asynchronous Procedure Call)直接调用。

5、进程间的通信

另外一种非常强大的用户模式与内核模式通讯方式,同时也支持进程间通讯,该方式就是ALPC(Advanced

Local Procedure Call),该方式被操作系统大量使用, WinRT中的Broker进程也用到了它。
该方式实际上就4个核心函数:nt!NtAlpcSendWaitReceivePort,nt!NtAlpcCreatePort, nt!

NtAlpcConnectPort, Nt!AplcAcceptConnectPort, 大概原理如下:
========

获取磁盘列表以及磁盘信息的一些WIN32 API

http://blog.csdn.net/cosmoslx/article/details/5769264
转自:http://www.cnblogs.com/imlee/archive/2007/09/26/906323.html

1.获取所有的驱动器
利用函数
GetLogicalDriveStrings
The GetLogicalDriveStrings function fills a buffer with strings that specify valid drives 
in the system.

DWORD GetLogicalDriveStrings(
  DWORD nBufferLength,  // size of buffer
  LPTSTR lpBuffer       // drive strings buffer
);

很简单的一个函数,msdn有详细的说明
需要注意的一点是

lpBuffer中最后获得的数据是这样c:/<null>d:/<null><null>,每两个路径之间都间隔一个 null-

terminated,
所以,如果你直接cout<<lpBuffer 的话,那么得到的是C:/,很是令人郁闷,于是要想办法把这些路径

一个一个取得

所以,有了如下代码
TCHAR szBuf[100];
memset(szBuf,0,100);
DWORD len = GetLogicalDriveStrings(sizeof(szBuf)/sizeof(TCHAR),szBuf);

for (TCHAR* s = szBuf; *s; s += _tcslen(s)+1)
{
 LPCTSTR sDrivePath = s;
 cout<<sDrivePath
}

那么这个sDrivePath 就是一个一个的类似于C:/,D:/那样的字符窜

2.获取驱动器类型
现在获得了驱动器的路径了,如C:/,D:/
那么如何区分他们呢,
有这个函数

GetDriveType
The GetDriveType function determines whether a disk drive is a removable, fixed, CD-ROM, 
RAM disk, or network drive.

UINT GetDriveType(
  LPCTSTR lpRootPathName   // root directory
);

UINT uDriveType = GetDriveType(sDrivePath);
调用以后,这个函数的返回值有

Value Meaning 
DRIVE_UNKNOWN                The drive type cannot be determined. 
DRIVE_NO_ROOT_DIR            The root path is invalid. For example, no volume is mounted at

the path. 
DRIVE_REMOVABLE               The disk can be removed from the drive. 
DRIVE_FIXED                     The disk cannot be removed from the drive. 
DRIVE_REMOTE                 The drive is a remote (network) drive. 
DRIVE_CDROM                   The drive is a CD-ROM drive. 
DRIVE_RAMDISK               The drive is a RAM disk.

但是,靠这个函数,很多东西,都是区分不了了,比如软驱,和插入的U盘,都是DRIVE_REMOVABLE ,而

硬盘和插入的移动硬盘,都是DRIVE_FIXED 
靠:(

我们一个一个来试试吧

3.获取光驱
先捏软柿子:)

UINT uDriveType = GetDriveType(sDrivePath);

if (uDriveType == DRIVE_CDROM)
{
 这个就是咯
}

要注意的是,虽然写的是DRIVE_CDROM
但是dvd 光驱也能获得(这不废话吗),另外,虚拟光驱也能获得,比如俺机器上安装了Alcohol 120%

,设置的虚拟光驱也获得了

4.区分软驱和U盘

先把代码贴出来吧
#define MEDIA_INFO_SIZE    sizeof(GET_MEDIA_TYPES)+15*sizeof(DEVICE_MEDIA_INFO)

BOOL GetDriveGeometry(const TCHAR * filename, DISK_GEOMETRY * pdg)
{
 HANDLE hDevice;         // 设备句柄
 BOOL bResult;           // DeviceIoControl的返回结果
 GET_MEDIA_TYPES *pmt;   // 内部用的输出缓冲区
 DWORD dwOutBytes;       // 输出数据长度

// 打开设备
 hDevice = ::CreateFile(filename,           // 文件名
  GENERIC_READ,                          // 软驱需要读盘
  FILE_SHARE_READ | FILE_SHARE_WRITE,    // 共享方式
  NULL,                                  // 默认的安全描述符
  OPEN_EXISTING,                         // 创建方式
  0,                                     // 不需设置文件属性
  NULL);                                 // 不需参照模板文件

if (hDevice == INVALID_HANDLE_VALUE)
 {
  // 设备无法打开...
  return FALSE;
 }

// 用IOCTL_DISK_GET_DRIVE_GEOMETRY取磁盘参数
 bResult = ::DeviceIoControl(hDevice,       // 设备句柄
  IOCTL_DISK_GET_DRIVE_GEOMETRY,         // 取磁盘参数
  NULL, 0,                               // 不需要输入数据
  pdg, sizeof(DISK_GEOMETRY),            // 输出数据缓冲区
  &dwOutBytes,                           // 输出数据长度
  (LPOVERLAPPED)NULL);                   // 用同步I/O

// 如果失败,再用IOCTL_STORAGE_GET_MEDIA_TYPES_EX取介质类型参数
 if (!bResult)
 {
  pmt = (GET_MEDIA_TYPES *)new BYTE[MEDIA_INFO_SIZE];

bResult = ::DeviceIoControl(hDevice,    // 设备句柄
   IOCTL_STORAGE_GET_MEDIA_TYPES_EX,   // 取介质类型参数
   NULL, 0,                            // 不需要输入数据
   pmt, MEDIA_INFO_SIZE,               // 输出数据缓冲区
   &dwOutBytes,                        // 输出数据长度
   (LPOVERLAPPED)NULL);                // 用同步I/O

if (bResult)
  {
   // 注意到结构DEVICE_MEDIA_INFO是在结构DISK_GEOMETRY的基础上扩充的
   // 为简化程序,用memcpy代替如下多条赋值语句:
   // pdg->MediaType = (MEDIA_TYPE)pmt->MediaInfo[0].DeviceSpecific.DiskInfo.MediaType;
   // pdg->Cylinders = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.Cylinders;
   // pdg->TracksPerCylinder = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.TracksPerCylinder;
   // ... ...
   ::memcpy(pdg, pmt->MediaInfo, sizeof(DISK_GEOMETRY));
  }

delete pmt;
 }

// 关闭设备句柄
 ::CloseHandle(hDevice);

return (bResult);

}

然后
DISK_GEOMETRY dg;
TCHAR szPath[100] = _T(".//");
::_tcscat(szPath,sDrivePath);
int nSize = ::_tcslen(szPath);
szPath[nSize-1] = '/0';

BOOL bRetVal = GetDriveGeometryszPath,&dg);

if(dg.MediaType == RemovableMedia)
{
   这就是U盘
}

这段代码,哇哦,好麻烦阿,好多看不懂,慢慢来
先看DISK_GEOMETRY 这个结构体

其中的MEDIA_TYPE是个枚举类型
具体就不列出来了,可以到msdn上察看到所有的

这里有一个很重要的函数,就是::DeviceIoControl,他可以获得很多属性

第一个参数是一个handle,我们要打开一个handle
调用::CreateFile,我晕,这不创建一个文件吗:)
其实这个函数,并不象我们想像中的那样,只能创建一个传统意义上的文件哦
这里我们用它“打开”设备驱动程序,得到设备的句柄。操作完成后用CloseHandle关闭设备句柄。

这里有以下小小的变化,如果路径是选择的是驱动器,那么这个路径的格式是要
//./DeviceName
比如
//./C:
真够bt的,
所以有了
TCHAR szPath[100] = _T(".//");
::_tcscat(szPath,sDrivePath);
int nSize = ::_tcslen(szPath);
szPath[nSize-1] = '/0';
这些代码
把C:/ => //./C:
关于::DeviceIoControl这个函数的用法,我不多说了,可以参考
http://dev.csdn.NET/article/55/55510.shtm这个系列,我也是参考这个的 ^+^

当然,这段代码,关于区分软驱和U盘,还有好多可以值得商榷的地方,比如,有人提出,软驱么,都在

A:的,比下路径不就得了,或者看大小1.44m
另外,软驱和U盘区分了,那么如果usb口上插的是别的东西呢,如读卡器,摄像头,怎么区分呢??
关于这个,确实还有很多值得我们去学习~~~~

4.区分移动硬盘和硬盘
我说了,移动硬盘也是DRIVE_FIXED ,真够bt的,这个没做过的话,很难想像的,太bt了
那怎么区分

用DeviceIoControl对卷下IOCTL_STORAGE_QUERY_PROPERTY进行获取信息
取返回STORAGE_DEVICE_DESCRIPTOR结构里面的STORAGE_BUS_TYPE

代码
#include <dbt.h>
#include <winioctl.h>
// IOCTL control code
#define IOCTL_STORAGE_QUERY_PROPERTY   CTL_CODE(IOCTL_STORAGE_BASE, 0x0500,

METHOD_BUFFERED, FILE_ANY_ACCESS)

typedef enum _STORAGE_PROPERTY_ID {
  StorageDeviceProperty = 0,
  StorageAdapterProperty,
  StorageDeviceIdProperty
} STORAGE_PROPERTY_ID, *PSTORAGE_PROPERTY_ID;

typedef enum _STORAGE_QUERY_TYPE {
  PropertyStandardQuery = 0, 
  PropertyExistsQuery, 
  PropertyMaskQuery, 
  PropertyQueryMaxDefined 
} STORAGE_QUERY_TYPE, *PSTORAGE_QUERY_TYPE;

typedef struct _STORAGE_PROPERTY_QUERY {
  STORAGE_PROPERTY_ID  PropertyId;
  STORAGE_QUERY_TYPE  QueryType;
  UCHAR  AdditionalParameters[1];
} STORAGE_PROPERTY_QUERY, *PSTORAGE_PROPERTY_QUERY;

typedef struct _STORAGE_DEVICE_DESCRIPTOR {
  ULONG  Version;
  ULONG  Size;
  UCHAR  DeviceType;
  UCHAR  DeviceTypeModifier;
  BOOLEAN  RemovableMedia;
  BOOLEAN  CommandQueueing;
  ULONG  VendorIdOffset;
  ULONG  ProductIdOffset;
  ULONG  ProductRevisionOffset;
  ULONG  SerialNumberOffset;
  STORAGE_BUS_TYPE  BusType;
  ULONG  RawPropertiesLength;
  UCHAR  RawDeviceProperties[1];
} STORAGE_DEVICE_DESCRIPTOR, *PSTORAGE_DEVICE_DESCRIPTOR;

HANDLE hDevice;         // 设备句柄
 BOOL bResult;           // DeviceIoControl的返回结果

// 打开设备
 hDevice = ::CreateFile(szPath,           // 文件名
  GENERIC_READ,                          // 软驱需要读盘
  FILE_SHARE_READ | FILE_SHARE_WRITE,    // 共享方式
  NULL,                                  // 默认的安全描述符
  OPEN_EXISTING,                         // 创建方式
  0,                                     // 不需设置文件属性
  NULL);

if (hDevice == INVALID_HANDLE_VALUE)
 {
     return FALSE;
 }

STORAGE_PROPERTY_QUERY Query; // input param for query
    DWORD dwOutBytes; // IOCTL output length

Query.PropertyId = StorageDeviceProperty;
 Query.QueryType = PropertyStandardQuery;

STORAGE_DEVICE_DESCRIPTOR pDevDesc;

pDevDesc.Size = sizeof(STORAGE_DEVICE_DESCRIPTOR);

// 用 IOCTL_STORAGE_QUERY_PROPERTY

bResult = ::DeviceIoControl(hDevice, // device handle
     IOCTL_STORAGE_QUERY_PROPERTY, // info of device property
     &Query, sizeof(STORAGE_PROPERTY_QUERY), // input data buffer
    &pDevDesc, pDevDesc.Size, // output data buffer
     &dwOutBytes, // out's length
      (LPOVERLAPPED)NULL);

UINT Type = pDevDesc.BusType;

//             Unknown                                                           0x00   
  //             SCSI                                                                 0x01

//             ATAPI                                                               0x02   
  //             ATA                                                                   0x03

//             IEEE1394                                                         0x04   
  //             SSA(Serial   storage   architecture)         0x05   
  //             Fibre   Channel,                                             0x06   
  //             USB,                                                                 0x07

//             RAID,                 0x08

这样,就能区分USB硬盘和普通硬盘了

参考资料
http://www.codeproject.com/w2k/usbdisks.asp

文章写到这里,我又回过头去试了一下,在3.区分u盘和软驱的时候,说实话,那个办法我不是很满意,

感觉有点小题大作了,搞得太复杂了,没办法,小弟实在是愚笨
其实用方法四中的查询,应该也是可以区分的,因为u盘的BusType是USB, 而软驱,我没法试了,因为机
器上木有软驱
但是我相信不会是USB的,
========

三种设备读写方式和I/O设备控制操作

http://www.blogfshare.com/buffer-direct-other.html

1.IRP的处理机制类似Windows应用程序中的 “消息处理”机制,驱动程序接受到不同的IRP后,会进入

不同的派遣函数,在派遣函数中IRP得到处理。

IRP(输入输出请求包),它是输入输出相关的重要数据结构,上层应用程序与底层驱动程序通信时,应

用程序会发出I/O请求。操作系统将I/O请求转化为相应的IRP数据,不同类型的IRP会根据类型传递到不

同的派遣函数内。可以使用IRPTrace来跟踪IRP。

2.三种读写方式:①缓冲区方式(DO_BUFFERED_IO) ②直接方式(DO_DIRECT_IO) ③其它方式 0

①缓冲区方式:操作系统将应用程序提供缓冲区的数据复制到内核模式下的地址中,这样,无论操作系

统如何切换进程,内核模式的地址都不会改变,IRP的派遣函数将会对内核模式下的缓冲区操作而不是操

作用户模式地址的缓冲区。

读写操作一般是由ReadFile和WriteFile函数引起的。例如,WriteFile要求用户提供一段带有数据的缓

冲区,并且说明缓冲区的大小,然后WriteFile将这段内存的数据传入到驱动程序中。

对于缓冲区读写方式来说,操作系统会将用户应用程序提供的缓冲区中的数据复制到内核模式下的地址

中。IRP的派遣函数将会对内核模式下的缓冲区进行操作,而不是操作用户模式下的缓冲区。对于

ReadFile来说,当IRP请求结束时(一般是由IoCompleteRequest函数结束IRP),这段内存地址会被复制

到ReadFile提供的缓冲区中,以此读出在内核中的数据。

这样做的优点是,比较简单的解决了将用户地址传入驱动的问题。缺点是需要在用户模式和内核模式之

间复制数据,影响了运行效率。在少量内存操作时,可以使用该方法。

以“缓冲区”方式读写设备时,操作系统会分配一段内核模式下的内存。这段内存大小等于ReadFile或

者WriteFile指定的字节数。并且ReadFile或者WriteFile创建的IRP的AssociatedIrp.SystemBuffer子域

会记录这段内存地址。

另外,在派遣函数中,我们还可以通过IO_STACK_LOCATION中的Parameters.Read.Length子域知道

ReadFile请求多少字节。通过中的Parameters.Write.Length子域知道WriteFile写入多少字节。

然而,WriteFile和ReadFile指定对设备操作多少字节,并不意味着操作了这么多字节。在派遣函数中,

应该设置IRP的子域IoStatus.Information。这个子域记录设备实际操作了多少字节。

而用户模式下的ReadFile和WriteFile分别通过各自的第四个参数得到真实操作了多少字节。

示例代码:

驱动:
NTSTATUS MyRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp)
{
    KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
     
    PIO_STACK_LOCATION pIrpStackLoc = IoGetCurrentIrpStackLocation(pIrp);
    //readLength 和 ReadFile函数中的第三个参数数值相同
    //是想要读取的字节数
    ULONG readLength = pIrpStackLoc->Parameters.Read.Length;
     
    //pIrp->IoStatus.Information的值就是ReadFile函数返回的第四个参数的值
    //是实际读取的字节数
    pIrp->IoStatus.Information = readLength;
    pIrp->IoStatus.Status = STATUS_SUCCESS;
     
    //填充内核模式下的缓冲区
    RtlFillMemory(pIrp->AssociatedIrp.SystemBuffer, readLength, 'A');
    //完成IRP
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
     
    KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
    return STATUS_SUCCESS;
}
 
应用程序:
#include <windows.h>
#include <stdio.h>
 
int main(void)
{
HANDLE hDevice;
hDevice = CreateFile("\\\\.\\HelloDDK",GENERIC_READ | GENERIC_WRITE,
           FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDevice == INVALID_HANDLE_VALUE)
{
           DWORD dwError = GetLastError();
           printf("%d\n", dwError);
}
    //多分配一个字节,使得printf可以读到'\0'结束
    char readBuffer[11] = {0};
    DWORD ulLength;
    ReadFile(hDevice, readBuffer, 10, &ulLength, NULL);
    printf("%s\n", readBuffer);
    CloseHandle(hDevice);
    getchar();
    return 0;
}
②直接方式:这种方式需要在创建完设备对象后,在设置设备属性的时候,对Flags子域设置为

DO_DIRECT_IO。

和缓冲区方式读写设备不同,直接方式读写设备,操作系统会将用户模式下的缓冲区锁住。然后操作系

统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式的缓冲区和内核模式的缓冲区指向的是

同一区域的物理内存。

操作系统先将用户模式的地址锁住后,操作系统用内存描述符表(MDL数据结构)记录这段内存。用户模式

的这段缓冲区在虚拟内存上是连续的,但是在物理内存上可能是离散的。如下图所示:

image

MDL记录这段虚拟内存,这段虚拟内存的大小存储在mdl->ByteCount里,这段虚拟内存的第一个页地址是

mdl->StartVa,这段虚拟内存的首地址对于第一个页地址偏移量为mdl->ByteOffset。因此,这段虚拟内

存的首地址应该是mdl->StartVa + mdl->ByteOffest。DDK提供了几个宏,方便我们得到这几个数值:

#define MmGetMdlByteCount(Mdl) ((Mdl)->ByteCount)

#define MmGetMdlByteOffsetMdl) ((Mdl)->ByteOffset)

#define MmGetMdlVirtualAddress (Mdl)

((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset))

我们通过IRP的pIrp->MdlAddress得到MDL数据结构,这个结构描述了被锁住的缓冲区内存。通过DDK的三

个宏MmGetMdlByteCount,MmGetMdlVirtualAddress,MmGetMdlByteOffset可以得到锁住缓冲区的长度,

虚拟内存地址,偏移量。

示例代码:

NTSTATUS MyRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp)
{
         KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
         //得到当前IO堆栈
         NTSTATUS status = STATUS_SUCCESS;
         PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
         //获取指定的读字节数
         ULONG ulReadLen = stack->Parameters.Read.Length;
         KdPrint(("ulReadLen:%d\n", ulReadLen));
         //得到锁定缓冲区的长度
         ULONG mdl_length = MmGetMdlByteCount(pIrp->MdlAddress);
         //得到锁定缓冲区的偏移量
         ULONG mdl_offset= MmGetMdlByteOffset(pIrp->MdlAddress);
         //得到锁定缓冲区的首地址,用户模式下地址
         PVOID mdl_address = MmGetMdlVirtualAddress(pIrp->MdlAddress);
        
         KdPrint(("mdl_address:0x%08X\n", mdl_address));
         KdPrint(("mdl_length:%d\n", mdl_length));
         KdPrint(("mdl_offset:%d\n", mdl_offset));
         //mdl的长度应该和要读取的长度相等,否则操作设为不成功。
         if (mdl_length != ulReadLen)
         {
                   pIrp->IoStatus.Information = 0;
                   status = STATUS_UNSUCCESSFUL;
         }
         else
         {
                   //用MmGetSystemAddressForMdlSafe得到MDL在内核模式下的映射
                   PVOID kernel_address = MmGetSystemAddressForMdlSafe(pIrp-

>MdlAddress,NormalPagePriority);
                   KdPrint(("kernel_address:0x%08X", kernel_address));
                   //填充内存
                   RtlFillMemory(kernel_address, mdl_length, 'B');
                   pIrp->IoStatus.Information = mdl_length;
         }
         //设置完成状态
         pIrp->IoStatus.Status = status;
         //结束IRP请求
         IoCompleteRequest(pIrp, IO_NO_INCREMENT);
         KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
         return status;
}
③其他方式读写设备

在调用IoCreateDevice创建设备后,对pDevObj->Flags即不设置DO_BUFFERED_IO,也不设置

DO_DIRECT_IO,此时采用的读写方式就是其他读写方式。

在使用其他方式读写设备时,派遣函数直接读写应用程序提供的缓冲区地址。在驱动程序中,直接操作

应用程序的缓冲区是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这

种方式。

用这种方式读写时,ReadFile和WriteFile提供的缓冲区内存地址,可以在派遣函数中通过pIrp-

>UserBuffer字段得到。需要读取的字节数可以从I/O堆栈中的stack->Parameters.Read.Length字段得到

使用用户模式的内存时要格外小心,因为ReadFile有可能把空指针地址或者非法地址传递给驱动程序。

因此,驱动程序使用用户模式地址前,需要探测这段内存是否可读写。探测可读写,可以使用

ProbeForWrite函数和try块。

示例代码:

NTSTATUS MyRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp)
{
         KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
 
         NTSTATUS status = STATUS_SUCCESS;
         //得到当前堆栈
         PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
         //得到要读取数据的长度
         ULONG ulReadLength = stack->Parameters.Read.Length;
         //得到用户模式下数据的地址
         PVOID user_address = pIrp->UserBuffer;
         KdPrint(("user_address:0x%08X\n", user_address));
        
         __try
         {
                   KdPrint(("进入__try块!\n"));
                   //测试用户模式下的地址是否可写
                   ProbeForWrite(user_address, ulReadLength, 4);
                   RtlFillMemory(user_address, ulReadLength, 'C');
                   KdPrint(("离开__try块!\n"));
         }
         __except(EXCEPTION_EXECUTE_HANDLER)
         {
                   KdPrint(("进入__except块!\n"));
                   status = STATUS_UNSUCCESSFUL;
                   ulReadLength = 0;
         }
         //设置完成状态
         pIrp->IoStatus.Status = status;
         //设置操作字节数
         pIrp->IoStatus.Information = ulReadLength;
         //结束IRP请求
         IoCompleteRequest(pIrp, IO_NO_INCREMENT);
 
         KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
         return status;
}
 
3.IO设备控制操作

除了用ReadFile(读设备)和WriteFile(写设备)以外,应用程序还可以通过另外一个WIN32 API函数

DeviceIoControl操作设备。DeviceIoControl内部会产生一个IRP_MJ_DEVICE_CONTROL类型的IRP,然后

操作系统会将这个IRP转发到派遣函数中。

我们可以用DeviceIoControl定义除读写以外的其他操作,它可以让应用程序和驱动程序进行通信。例如

,要对一个设备进行初始化操作,程序员自定义一种I/O控制码,然后用DeviceIoControl将这个控制码

和请求一起传递给驱动程序。在派遣函数中,分别对不同的I/O控制码进行处理。

BOOL DeviceIoControl(

HANDLE hDevice, // 已经打开的设备

DWORD dwIoControlCode, // 控制码

LPVOID lpInBuffer, //输入缓冲区

DWORD nInBufferSize, //输入缓冲区大小

LPVOID lpOutBuffer, // 输出缓冲区

DWORD nOutBufferSize, // 输出缓冲区大小

LPDWORD lpBytesReturned, // 实际返回字节数

LPOVERLAPPED lpOverlapped //是否异步操作

);

其中,lpBytesReturned对应派遣函数中的IRP结构中的pIrp->IoStatus.Information。

dwIoControlCode是I/O控制码,控制码也称IOCTL值,是一个32位的无符号整形。IOCTL需要符合DDK的规

定。

image

DDK提供了一个宏CTL_CODE,方便我们定义IOCTL值,其定义如下:

CTL_CODE(DeviceType, Function, Method, Access)

DeviceType:设备对象的类型,这个设备应和创建设备(IoCreateDevice)时的类型相匹配。一般形式如

FILE_DEVICE_xxxx的宏。

Function:这是驱动程序定义的IOCTL码。其中:0X0000-0X7FFF为微软保留。0X8000到0XFFFF由程序员

自己定义。

Method:这个是操作模式。可以是以下四种模式的一种:

(1) METHOD_BUFFERED:使用缓冲区方式操作

(2) METHOD_IN_DIRECT:使用直接写方式操作

(3) METHOD_OUT_DIRECT:使用直接读方式操作

(4) METHOD_NEITHER:使用其他方式操作

Access:访问权限,一般为FILE_ANY_ACCESS

①缓冲内存模式IOCTL

使用这种模式时,在Win32 API 函数DeviceIoControl的内部,用户提供的缓冲区的内容会被复制到IRP

中的pIrp->AssociatedIrp.SystemBuffer内存地址,复制的字节数是由DeviceIoControl指定的输入字节

数。

派遣函数可以读取pIrp->AssociatedIrp.SystemBuffer的内存地址,从而获得应用程序提供的输入缓冲

区。另外,派遣函数还可以写入pIrp->AssociatedIrp.SystemBuffer的内存地址,这被当做设备输出的

数据。操作系统会将这个地址的数据再次复制到DeviceIoControl提供的输出缓冲区中。复制的字节数有

pIrp->IoStatus.Information指定。而DeviceIoControl可以通过它的第七个参数得到这个操作字节数。

派遣函数先通过IoGetCurrentStackLocation函数得到当前I/O堆栈。派遣函数通过stack-

>Parameters.DeviceIoControl.InputBufferLength得到输入缓冲区的大小,通过stack-

>Parameters.DeviceIoControl.OutputBufferLength得到输出缓冲区的大小。最后通过stack-

>Parameters.DeviceIoControl.IoControlCode得到IOCTL。在派遣函数中通过switch语句分别处理不同

的IOCTL。

示例代码:

//----------------
定义IOCTL:
#define CTL_TEST1 CTL_CODE(\
              FILE_DEVICE_UNKNOWN, \
              0X800, \
              METHOD_BUFFERED, \
              FILE_ANY_ACCESS)
 
Win32测试程序:
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include "..\NT_Driver\ioctl.h"
 
int main(void)
{
         HANDLE hDevice;
         //打开设备
         hDevice = CreateFile("\\\\.\\HelloDDK", GENERIC_READ| GENERIC_WRITE,
                   0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 
         if (hDevice == INVALID_HANDLE_VALUE)
         {
                   printf("CreateFile Error : %d\n", GetLastError());
                   return -1;
         }
 
         BOOL bRet;
         CHAR inBuffer[10];
         memset(inBuffer, 'B', sizeof(inBuffer));
 
         CHAR outBuffer[10];
         DWORD dwReturn;
         bRet = DeviceIoControl(hDevice, CTL_TEST1, inBuffer, sizeof(inBuffer),
                                                                           &outBuffer, 
sizeof(outBuffer), &dwReturn, NULL);
 
         if (bRet)
         {
                   for (int i=0; i<(int)dwReturn; i++)
                   {
                            printf("%c ", outBuffer[i]);                
                   }
                   printf("\n");
                   return 0;
         }
         else
                   return -1;
}
 
驱动层:
NTSTATUS MyDeviceControl(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp)
{
         KdPrint(("进入IRP_MJ_DEVICE_CONTROL处理函数!\n"));
        
         NTSTATUS status = STATUS_SUCCESS;
         PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
         ULONG inLength = stack->Parameters.DeviceIoControl.InputBufferLength;
         ULONG outLength = stack->Parameters.DeviceIoControl.OutputBufferLength;
         ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
 
         switch(code)
         {
         case CTL_TEST1:{
                            KdPrint(("CTL_TEST1\n"));
                            CHAR* inBuffer = (CHAR*)pIrp->AssociatedIrp.SystemBuffer;
                            for (int i=0; i<(int)inLength; i++)
                            {
                                     KdPrint(("%c ", inBuffer[i]));
                            }
 
                            CHAR* outBuffer =(CHAR*) pIrp->AssociatedIrp.SystemBuffer;
                            RtlFillMemory(outBuffer, outLength, 'A');
                            break;
                   }
         default:
                   status = STATUS_INVALID_VARIANT;
         }
 
         pIrp->IoStatus.Information = outLength;
         pIrp->IoStatus.Status = status;
         IoCompleteRequest(pIrp, IO_NO_INCREMENT);
         KdPrint(("离开IR_MJ_DEVICE_CONTROL处理函数!\n"));
 
         return status;
}
②直接内存模式IOCTL

当使用这种模式时,在用CTL_CODE宏定义这种IOTL时,应该制定Method参数为METHOD_OUT_DIRECT或者

METHOD_IN_DIRECT。直接模式的IOCTL同样可以避免驱动程序访问用户模式的内存地址。

METHOD_IN_DIRECT: if the caller of DeviceIoControl or IoBuildDeviceIoControlRequest will

pass data to the driver.

METHOD_OUT_DIRECT: if the caller of DeviceIoControl or IoBuildDeviceIoControlRequest will

receive data from the driver.

在调用DeviceIoControl时,输入缓冲区的内容被复制到IRP中的pIrp->AssociatedIrp.SystemBuffer内

存地址,复制的字节数由DeviceIoControl指定。这个步骤和缓冲区模式的IOCTL的处理时一样的。

但是当对于DeviceIoControl指定的输出缓冲区的处理,直接模式的IOCTL和缓冲区模式的IOCTL却是以不

同方式处理的。操作系统会将DeviceIoControl指定的输出缓冲区锁定,然后在内核%E

win32 IOCTL学习总结相关推荐

  1. Win32汇编学习——windows汇编语法(小甲鱼教程)

    Win32汇编学习--windows汇编语法(小甲鱼教程) 1)指令集 .386 语句是汇编语句的伪指令,类似指令有:.8086 . .186  ..286  ..386/.386p  . .486/ ...

  2. Win32 多线程学习总结

    Win32多线程编程学习心得 http://blog.csdn.net/jonathan321/article/details/50782832 博客原文地址:http://jerkwisdom.gi ...

  3. java 调用win32 api 学习总结

    java使用JInvoke调用windows API 使用jinvoke调用windowsAPI.jna使用比较麻烦,需要写c代码和参数转换,jinvoke的使用就像jdk中的包一样. 官网使用参考: ...

  4. Win32 GDI 学习总结

    Windows GDI 教程(一) 一个简单的绘图程序 http://www.tuicool.com/articles/jeMBZ3v 常见的图形编程库,除了 GDI 外还有 GDI+.OpenGL. ...

  5. win32汇编语言学习笔记(三)

    汇编语言学习笔记(三) CH3.Windows汇编基础 .386 .model flat,stdcall option casemap:none 定义程序使用的指令集.工作模式 相应的还有:.8086 ...

  6. C#调用Win32 api学习总结

    转载:https://blog.csdn.net/bcbobo21cn/article/details/50930221 从.NET平台调用Win32 API Win32 API可以直接控制Micro ...

  7. Win32汇编学习(10):对话框(1)

    现在我们开始学习一些有关GUI编程的有趣的部分:以对话框为主要界面的应用程序. 理论: 如果您仔细关注过前一个程序就会发现:您无法按TAB键从一个子窗口控件跳到另一个子窗口控件,要想转移的话只有 用鼠 ...

  8. win32 DLL 学习总结

    DLL的开发与调用(一)--创建导出函数的Win32 DLL http://www.cnblogs.com/Pickuper/articles/2053745.html Visual C++6.0 中 ...

  9. C语言调用WIN32 API学习之6鼠标与键盘响应

    前几节学习了基本控件的创建,下面学习下鼠标与键盘的响应 1,打开VC++6.0,点击 文件->打开工作空间 选择example1,点击确定,打开工程. 2,更改代码如下: LRESULT CAL ...

最新文章

  1. Java面试题-javaweb篇七
  2. 等号赋值与memcpy的效率问题
  3. mysql8.0.17压缩包安装教程_超详细的MySQL8.0.17版本安装教程
  4. Re:从零开始的Spring Session(二)
  5. python运行调出控制台_python.exe 和 pythonw.exe 的区别
  6. 声音均衡器怎么调好听_汽车10段音效最佳设置,手把手教你调节车载音响均衡器...
  7. python接收邮件内容启动程序_如何使用python获取电子邮件的文本内容?
  8. Apache 服务器端安装配置(Windows版本)
  9. 乱七八糟 Nodejs 系列一:试水
  10. 使用phpQuery获取数组
  11. 华为无线ensp跨ac三层漫游
  12. 显著性水平与p值的区别
  13. web小说目录倒序php实现,php实现WEB在线文件管理器
  14. 自定义input单选框样式
  15. 内网信息收集(手动收集本机信息)
  16. 牛客网刷题java之(斐波那契数列)一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
  17. MD5算法原理简要介绍并采用C#应用在桌面应用系统的用户登录与注册中
  18. Android启动活动用什么方法,Android - 使用intent uri从命令行启动活动
  19. 关于程序猿 59 条搞笑但却真实无比的语录
  20. hive sql 实现姓名手机号证件号脱敏

热门文章

  1. 把激光点投影到图像上并融合显示
  2. 12月世界滋补产业生态大会,走进燕窝滋补品产业新领域
  3. 腾讯QQ下线付费入群功能
  4. 以太网学习之二 物理介质(10Base、100Base-T、100Base-TX等)
  5. [android] 手机卫士接收打电话广播显示号码归属地
  6. 第十七期 U-Boot norflash 操作原理分析 《路由器就是开发板》
  7. innovus: 合并macro lef与antenna lef
  8. mysql修改max_allowed_packet
  9. Android传感器【转】
  10. 数据库原理及应用教程(第4版|微课版)陈志泊-第四章习题