day3 windows核心编程总结 第一部分

第一章 错误码

1、利用GetLastError获得程序上一次的错误代码,程序调用成功也可能会覆盖GetLastError的结果(创建重复返回arelay_erorr)

WinError.h包含所有的错误代码值,Microsoft不会维护一个主控列表,一个window的函数里面嵌套的windows函数,不会每一个函数都返回错误代码

2、工具:一、调试的时候可以用vs的watch,输入$err,hr;二、Erorr_Lookup小程序可以将错误代码id转化成响应的文本描述

三、程序上可以使用FormatMessage将错误代码id转化成相应的文本描述,还可以改变文本描述编译好消息表嵌入自己的exe或者dll;

3、如果想在日志中打印win系统函数的报错信息:1、GetLastError获取错误代码->FormatMessage翻译成默认英文或者自定义描述->写到日志。自定义描述怎么做,以后百度去。

第二章 字符和字符串处理

1、字符串默认最后+\0,使用字符串初始化字符数组的时候,数组最后有\0;但是用一个个字符去初始化字符数组的时候,数组最后没有\0;这点要清楚;

2、_countof是计算静态分配的数组的元素个数,sizeof()是数组的整个长度

3、Ansi和Unicode关系

老字符集版本:ACSII,只对他们英文的字符编了标准编号(00~7f这种16进制编号),用一个字节存这个编号,

一个字节8个位,00~ff完全足够,他们只占用00~7f(十进制0~127),还有7f~ff的最后一个高八位没用(范围是128~256)

新字符集版本:Unicode,对全世界各种语言的符号都编了标准编号,用两个字节中存这个编号, 0000~ffff 的范围

宽字符(wchat)普通字符(char):Unicode(宽字符 = 两个字节 = 16个位),ASCII(普通字符 = 一个字节 = 8个位)

Ansi是老字符ACSII在不同的国家通过各种能工巧匠的各种骚操作修改成自己国家的老字符版本,俗称被本土化的ACSII表

比如我们国家的Ansi就是GB2312编码格式,就是利用7f~ff这块区域放置了常用的中文字符,一个汉字,是用两个字符(两个ansi)表示,比如"佳"," 亻"用一个字节(7f~ff),"圭"也用一个字节(7f~ff),两个字符的7f~ff之间的16进制编号可以组成汉字

日本或者韩国也有属于自己的Ansi版本字符,中国Ansi的是GB2312,至于GBK和GB18083都是对GB2312的扩展,扩展了繁体,不常见的生字,和少数民族的一些字符

Ansi在美国叫ACSII,在中国叫GB2312,一个地方一个名字了,但是要记住他们是老字符,有区域限制,比如我们中国的程序用的GB2312,放到日本,他们那没有GB2312的库的话,显示的汉字就是乱码。win7是美国的操作系统,为什么可以正常显示汉字呢,其实一开始最早的微软计算机也是乱码,需要装GB2312库才正常。win7,win10应该都安装支持GB2312.

但是如果想和国际接轨,比如中国的程序和韩国的程序进行交互,那么汉字和韩文,就要在两种老字符集,中国的GB2312和韩国的****进行繁琐的转换

这个时候就要用Unicode新字符集来编码啦,先看一下老字符集(ASCII)网上可查

老字符集:

01000001

101

65

41

A

大写字母A

新字符集:

0000000001000001

 

101

65

0041

A

大写字母A

 

老字符集中(ASCII):A的16进制编码是0x41,10进制编码是65,二进制是0100 0001

新字符集中(UNICODE):A的16进制编码是0x0041,10进制编码是65,二进制是0000 0000 0100 0001

可见A在新老字符变化中并不是很大(英文字符还是排在前面),区别是使用了两个字节。

但是汉字区别就很大。

老字符集中(ASCII):比如"中", 用16进制编码0xd6 0xd0组合表示,因为汉字是两个字符组合表示

新字符集中(UNICODE):“中”,用16进制编码0x4e2d表示,但是这里是宽字符,一个宽字符16位

 

可以看出汉字用unicode新字符,还是很划算的,都是16位。但是英文用新字符集,占用16位就比较亏,前面8位都没用,内存占用不合理,浪费了一半空间。

 

utf-8和Unicode的关系:

uft-8是Unicode的另外一种表现方式,作用是优化Unicode占用内存不合理的问题,实现英文也可以用8位表示。

注意:

1、uft-8采用的是新字符集,全世界通用,可以想象成是Unicode变身形态。

2、uft-8采用的不是宽字节,而是单字节,跟ASCII一样单字节,但是用的字符集是新字符集,新字符库。(本质不同)

3、utf-8可以根据不同国家的语言,变化字节数,英文->1个,中文->3个

举个例子:我们看看"中"在unicode和utf-8中的不同表现吧

UNICODE:用16进制编码0x4e2d表示,一个宽字符(wchar)

UTF-8:用16进制编码 0xe4 0xb8 0xad表示,三个普通字符(char)

ASCII: 用16进制编码0xd6 0xd0组合表示,两个普通字符(char)

 

从上也看出,不同的编码其实本质是该符号与其16进制编码号的对应不同

虽然使用uft-8表示中文(三个普通字符),对内存空间并不占优势,甚至还增加内存,但是对于其他语言,英文(一个字符)等等的字符,使用utf-8可以节约内存开销。

 

utf-16和Unicode的区别:

我此时此刻的水平,认为他们是一样的utf-16=Unicode,他们的表示出来的16进制编码是一样的。

 

win2000以上的windows内核都使用Unicode新字符集来写的,接受宽字符,所以如果字符还是uft-8的形式,去调用windowApi的

函数接口,比如CreateWindowsExW,那就行不通了,所以在还要转化为宽字符去调用CreateWindowsExW。

笔者没接触过Unicode项目,大胆猜测,Unicode项目基本使用都是的是宽字符,传输和文本读取储存的时候,会将字符转化uft-8(普通字符)来传输和储存(节省空间内存),然后传输完毕后,将字符转化为Unicode(也可以说是utf-16,宽字符),放到程序中使用。

读取uft-8的文本的时候,也要将uft-8转化为Unicode(uft-16)。

国内的大部分程序都是GB2312字符集的,就是老的字符集,中国版的ASCII改版,中国版的Ansi。GB2313的程序有时候也使用uft-8来转换,为什么呢,这是因为程序接受到另一个程序发来的字符串,如果发的程序里面发送的存在utf-8编码的字符,如果要显示正常中文,那么就必须转成GB2312才行。

简单来说,就是要将外界发过来的字符的编码转化为本地程序的字符编码,在有必要的情况下。

---------------------------------------------------------------------------------------------------------------------------------

再来说下Unicode和Ansi的win函数,_w的是Unicode,_A的是Ansi,但是_A函数内部实际是将字符转化为Unicode字符,然后调用_W,然后再转回Ansi字符返回。

C中的Unicode和Ansi函数区别:

t_+函数名,根据项目设定的字符集自动跳转。

t_strlen,Unicode->wstrlen,Ansi->strlen

字符串操作控制:StringCchCat,StringCchCopy,comepareString等等

 

第三章 内核对象

内核对象:文件对象,文件映射对象,互斥锁对象,事件对象,进程对象,管道对象,信号量对象,线程对象,I/O对象,访问令牌对象,可等待计时器对象,线程池工厂对象。

工具1:winObj查看内核对象名称

引用计数:内核对象创建时候+1,CloseHandle的时候-1,当为0的时候销毁内核对象。在进程之间公用同一个内核对象的时候,引用计数会++,后面会讲,有三种方式。

typedef struct _SECURITY_ATTRIBUTES {
    DWORD nLength;
    LPVOID lpSecurityDescriptor;
    BOOL bInheritHandle;
} SECURITY_ATTRIBUTES

安全描述符:SECURITY_ATTRIBUTES结构体,里面可以传安全描述符,这个描述符可以决定哪些用户或者用户组对此内核的访问权限,要注意去访问内核对象的时候,调用的函数是否有针对不同用户权限的区别,比如程序像注册表获取数据,调用RegOpenKeyEx,传递参数,Key_ALL_Access,获取全部注册表权限,但是管理员是可以获得的,但是普通用户并不能获得这个权限,所以就会报错,所以应该传递一个Key_Query_Value这样一个只读的权限,这个只读权限是可以所有用户都可以获取的。也可以使用安全描述符来判断这个对象是否是内核对象,内核对象基本都需要传递一个这个安全描述符。用好这个安全符号,有利于我们在不同的windows版本中移植。

句柄表:

格式:         索引值(从1开始,索引)  内存地址    访问掩码 (未知用处)   标志(用于继承,可能还有其他的用处)

句柄表只给内核对象使用,其他的对象没有,句柄表实质是一个结构体的一个数组

Struct JuTable

{

 unsigned int index;

void*    pAddress;

 unsigned int mask;

 unsigned int flag;

}

句柄表:JuTable[TABLE_COUNT]

句柄值:调试程序的时候看到的handle值,就是调用内核对象返回的值。

索引值:在句柄表里实际的key值,索引值=句柄值/4,所以经常我们看到的handleid是4的倍数

句柄值返回失败:一般返回0(NULL),有一些返回INVAILD_HANDLE_VALUE(-1),根据具体的内核对象来进行判空还是判无效HANDLE,判空注意。一般创建内核对象返回是0(NULL),可能是内核内存空间或者安全问题导致创建失败。另一种是类似createfile创建文件内核对象的时候,如果无法打开文件,就会返回INVAILD_HANDLE_VALUE(-1)。

关闭内核对象:调用CloseHandle(),引用计数-1,当内核引用计数为0的时候,销毁内核对象。但是当这个内核对象在其他进程的存在的时候,由于引用计数也++,所以就算本进程CloseHandle,内核对象仍不会销毁,除非其他进程都CloseHandle,使引用计数归零。

值得注意的是:CloseHandle(handle)后不要再使用这个handle,并且马上handle = NULL处理。原因:1、CloseHandle后,内核对象可能被销毁,也可能没被销毁(未来销毁)。2、如果handle没有被置空,可以被程序继续使用,那么程序使用的是一个被销毁的内核对象或者未来要被销毁的内核对象(内核对象存在于其他进程),危险有2:1、程序根据handle句柄表值,查找句柄表,发现这条记录被擦除,从内存中移除,那么会返回FALSE 2、本条句柄值记录被擦除后,被其他内核对象插入,具有相同的句柄值,但是内存地址和内核对象是不一样的,会导致不可控的结果。

比如你CloseHandle一个进程对象,并且继续使用进程对象句柄表,但是实际的这个对象已经被销毁,并且另一个刚刚创建的比如线程对象插入刚刚的销毁的进程对象位置,这样句柄值一样,因为位置一样,但是指向的内存和对象功能,类型全都不一样了。

假如我们忘记CloseHandle怎么办,答案是在程序运行过程中,会造成内存泄露,句柄表里的那条记录一直存在,内存块都在,那进程关了怎么办?这里有个清除机制,进程关闭的时候,查询遍历句柄表,并且一个个进行关闭(类似CloseHandle),将引用计数-1,引用计数为0的时候,计算机内核销毁该内核对象。

工具2:查看进程的句柄数:任务管理器。查看句柄内存泄露:process explorer

进程之间公用同一个内核对象的三种方式

方式一:父子进程使用继承内核对象句柄的方式

两个条件

1、内核对象是可继承的,创建内核对象的时候,传入安全描述符,里面有个bInheritHandle,将它置true,储存在句柄表里的标志那里,1是可继承,0是不能继承

2、在父进程里面创建子进程,调用CreateProress,并且把内核对象句柄作为命令行参数传入,将参数bInheritHandle置true

还有一种是设置父进程环境变量(把句柄值放入环境变量),子进程继承父进程的环境变量,也可以获得句柄值

创建子进程后,子会创建一个空白的句柄表(这时不会执行子进程代码),当第二个条件满足的时候,父进程会遍历它的句柄表,找出满足条件一的可继承的内核对象,然后把他们打包,复制到子进程的句柄表里,此时,句柄值,内存值,访问掩码和标志全都和父进程一模一样,重点是为了保持句柄值(索引值)一样,所以子进程的句柄表的初始排列可能并不是连续的,有空隙。引用计数也会+1。要销毁的时候,也要且确保父子进程都退出或者都调用CloseHandle,使计数为0

改变内核对象的继承性,也就是改变句柄表的标志位那列的值

函数:SetHandleInformation(HANDLE hObject,DWORD dwMask,DWORD dwFlag)

dwMask有两个标志位,HANDLE_FLAG_INHERIT,HANDLE_FLAG_PROTECT_FROM_CLOSE

SetHandleInformation(hObject,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT)->内核对象继承性为真

SetHandleInformation(hObject,HANDLE_FLAG_INHERIT,0)->内核对象继承性为假

SetHandleInformation(hObject,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE)->closeHandle的时候会返回false(调试的时候返回异常),但是并不阻碍内核可以加判断打印日志,保证不Closehandle

SetHandleInformation(hObject,HANDLE_FLAG_PROTECT_FROM_CLOSE,0)->关闭这个保护,CloseHandle的时候就不会返回fasle

获取内核对象的信息:GetHandleInfomation,获得的返回值是一个叠加值。如果需要判断继承性,只需要判断(返回值&HANDLE_FLAG_INHERIT)  !=0即可,其他的属性那就&其他属性。

方式二:使用名称创建或者打开的内核对象

比如两个进程都创建了"MyMutex"的互斥锁,那么这两个进程都关联了同一个名字叫"MyMutex"的互斥锁内核对象,这个互斥锁对象是同一个,两个句柄表里的记录的这个互斥锁对象在内核中的地址是一样的,访问掩码以及继承性也一样,唯一不同的是在各自的句柄表里的句柄值不同。因为这种形式创建内核对象,在进程之间共享,不需要依赖句柄值相同来相同一个内核对象,依赖的是内核对象的名称,也就是刚刚说的"MyMutex"。

这里就会出现一个安全问题有木有:比如A,B进程,A进程是跟钱有关的进程,B进程是我们的盗窃进程,假如B进程猜到的我们的A进程里面一个重要对内对象的名称(不一定是猜到,也可能用其他方法,应该方法很多),B进程用这个名称create或者open这个内核对象,那就到这个重要的内核对象并且拿到相应的权限,那B这个流氓进程就可以为所欲为了。

Create*:参数一般有安全描述结构_SECURITY_ATTRIBUTES,内核对象名称pszName。如果创建的时候已经存在一个内核对象,那么安全描述符会失效,安全描述符的内容根据已经存在的内核对象来定。如果创建一个已经存在的内核对象,也可能失败,因为可能类型不用,结果返回NULL。如果想判断create*的时候是不是第一次创建,可以判断getlasterror == ERROR_ALREADY_EXIST

Open*:参数一般有接受符dwDesiredAccess,继承性InheritHandle和pszName名称,如果调用成功,会更新本进程的句柄表,和create一样。不一样的地方是Open可以改变继承性,InheritHandle参数改变本进程句柄表的该内核对象标志,也就是本进程的该对象的继承性。那么以前的那个进程的内核对象,继承性会被修改么,首先内核对象的内存地址都是同一份,那么是同一个内核对象,按照道理,应该以前的内核对象也被修改了,但是一个进程被另一个进程修改共享内核对象的继承性很危险诶,所以此点持怀疑态度,没证实,以后再说

Create*和Open*都是通过名称的方式实现了内核对象的共享,区别是Create*如果对象不存在,它会创建它;而Open*则会返回失败。

实例判断:如果你想一个程序只能在当前用户下只能运行一个,那么可以使用这个方法:用名称Create*一个任意类型的内核对象,如果另一个相同程序也在运行(说明该内核对象早已存在),那么判断getlasterror == ERROR_ALREADY_EXIST?是的话就直接返回退出main函数。

刚刚说了安全问题,如果程序都在同一个命名空间,那么势必会造成程序之间的攻击,利用名称获得共享的内核对象,所以这里windows有一个安全机制:为内核对象设定专有命名空间,保证了名称的唯一性,后面会详细介绍它怎么就安全了。

专有命名空间:简单来说就是为一个内核对象提供另一个高级的专有命名空间,比如我们用名称创建一个互斥锁内核对象CreateMutex,需要传入安全描述如sa和名称

普通情况 :CreateMutex(&sa,"MyMutex");

专有命名空间情况:CreateMutex(&sa,"NameSpace-1\MyMutex");名称格式:专有命名空间描述符+"\"(这个是转义把后面的\符号转义成真的\)+"\"+名称

介绍一下用专有空间步骤

1、创建/打开专有命名空间,如果没有这步骤的话,创建的内核对象仍然在本地普通的命名空间内

2、名称=专有命名空间描述符+"\"+原始名称,类似上面的:"NameSpace-1\MyMutex",专有命名空间描述符可以自定义

个人理解:创建/打开专有命名空间的作用:这个内核对象的名称就到了另一个命名空间内,会在这个名称前再加属于自己的前缀;这个前缀可能是地址符也可能是其他的字符组合,最后的到真正创建内核的时候可能是专有命名空间前缀+描述符+"\'"+原始名称:"一串前缀密文\NameSpace-1\MyMutex"(书上的专有命名描述是..\)。其实这里的内核对象的专有命名空间很像我们常用的namespace std;不同的命名空间可以存相同的字符串。

接下来我们看看创建专有命名空间的步骤

1、创建专有命名空间的边界:CreateBoundaryDescriptor(g_szBoundary, 0);g_szBoundary是个边界描述符,可以自定义

2、创建特权用户组SID:  CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, pLocalAdminSID, &cbSID),WinBuiltinAdministratorsSid参数是代表需要创建管理员(Administrators)权限的SID

3、将边界和SID绑定:AddSIDToBoundaryDescriptor(&g_hBoundary, pLocalAdminSID)) ,这样边界就有了权限用户组的信息

4、使用边界创建专有命名空间:CreatePrivateNamespace(&sa, g_hBoundary, g_szNamespace);g_szNamespace是专有命名名空间描述符,可以自定义,就是上面说的"NameSpace-1”,g_hBoundary是边界,sa是安全描述结构

第一看到sa是怎么初始化的,贴一下

   SECURITY_ATTRIBUTES sa;
   sa.nLength = sizeof(sa);
   sa.bInheritHandle = FALSE;
   if (!ConvertStringSecurityDescriptorToSecurityDescriptor(
      TEXT("D:(A;;GA;;;BA)"), 
      SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL)) { //sa.lpSecurityDescriptor这个是真正的描述符结构,里面和SID也有关
      return;
   }

https://baike.baidu.com/item/ConvertStringSecurityDescriptorToSecurityDescriptor/9308942?fr=aladdin度娘地址

安全描述结构和SID之类的以后再深入研究

最后来看一下终端(远程桌面,快速用户切换)服务命名空间

顾明思议,这是针对终端服务开辟的命名空间,跟会话相关;在终端这种类型计算里面,所以跟上面很大不同。

这个终端服务命名空间有两种命名空间:

1.全局命名空间,所有客户端,所有会话都能访问到这个命名空间。一般是服务来用。比如每个客户端调用的某某服务。

2:属于客户端会话自己独有的命名空间。一个会话,一个命名空间。互相不能访问。就算创建名称相同的内核对象,因为每个客户端会话的命名空间不同,所以他们得创建同名内核对象也是不同的,可以做到互不影响。

系统服务程序(启动后有较为安全的机制和权限)会在会话0中先启动,其他客户端会话启动后,就算有恶意程序,他们也是后面启动,去攻击系统服务程序也难了。虽然他们共享全局命名空间,但是系统服务器程序我觉得可能有上述说的专有命名空间来做到更好的安全性。

一句话:终端或者远程桌面合作和快速用户切换的情况下,需要客户端会话和客户端会话通信共享内核对象或者跟服务共享内核对象,比如用全局命名空间。

获得会话id:ProcessIdToSessionId(当前进程id,&得到的会话id)

强制改变命名空间:

转全局命名空间名称格式:“”Global“”+"\"(这个是转义把后面的\符号转义成真的\)+"\"+内核对象名称

转当前会话自己独有命名空间名称格式:“”Local“”+"\"(这个是转义把后面的\符号转义成真的\)+"\"+内核对象名称;还可以Session\<当前会话id>\+"\"+内核对象名称的形式

这三个关键字都是保留字段,一般不使用,除非要做特定的某些功能需要改这些命名空间,三个关键字区分带大小写。

方式二:赋值内核对象句柄

BOOL WINAPI DuplicateHandle(

__in HANDLE hSourceProcessHandle,//源进程内核对象句柄

__in HANDLE hSourceHandle,//源要赋值的内核对象句柄

__in HANDLE hTargetProcessHandle,//目标进程内核对象句柄

__out LPHANDLE lpTargetHandle,//目标要赋值的内核对象句柄

__in DWORD dwDesiredAccess,新句柄要求的安全访问级别(也就是访问掩码)。如dwOptions已指定了DUPLICATE_SAME_ACCESS,那么忽略这里的设置。可以进行的访问由对象的类型决定,它们在不同系统对象的访问常数表里进行了总结

__in BOOL bInheritHandle,//继承性,是否改变原进程记录表里的继承性(标志位)待定

__in DWORD dwOptions//下列常数的一个或两个:

DUPLICATE_SAME_ACCESS 新句柄拥有与原始句柄相同的安全访问特征

DUPLICATE_CLOSE_SOURCE 关闭原始句柄,引用计数不改变(因为原始关闭-1,新进程+1),如果原始句柄已经关闭。即使发生错误。它也要关闭

);

常见的用法是三个进程之间,A,B,将A的内核对象赋值给B。

DuplicateHandle调用成功会把A进程的句柄表的这条内核对象记录复制到B进程的空白句柄表记录。这里注意,A进程和B进程的该内核对象的句柄值是不同的。

那B进程怎么知道它获得了一个句柄值可以访问内核对象了?书上没说,我猜B这里应该有个消息回调吧。

CloseHandle的时候要注意是不是为0了并且被销毁了,如果继续用的这个句柄要注意

DuplicateHandle还可以在同一个进程内改变内核对象的访问属性,其实是拷贝出一个副本,引用计数++,用这个改变访问属性的副本去做事情,书上例子:文件映射属性读写,现在复制副本只读属性,然后根据需要来用副本或者原本,再根据需要去销毁。

 

 

 

 

 

 

 

 

 

 

     

你可能感兴趣的:(day3 windows核心编程总结 第一部分)