Windows编程基础

旧文迁移,新年第一篇,稍微回顾(bu)了下C(翁凯老师的C进阶),以后遇到了再记上来吧~

C复习

地址

&取地址,只能对存在地址的变量取地址

指针

定义时,*代表类型是指针

1
2
int* p, q;
int *x, y;

使用时,*代表取存储的地址处的数据
应用场景:
函数要改变传入值,函数要返回多个结果数据(return返回运算状态)
常见错误:
定义指针类型后未赋值就开始使用->里面存的值未初始化,使用即在错误的地址取值

1
2
3
4
5
//这四种是等价的,只是对于无名参数只能在声明函数时使用
RESULT funA(int a[]);
RESULT funB(int[]);
RESULT funC(int *a);
RESULT funD(int *);

数组名是指针常量,于是每个元素都是常量

1
2
const int a[4] = { 1,2,3,4 };
a[1] = 3;//错误

常量

const的作用范围仅限于直接对象,例如int * const q代表指针q是常量,即里面保存的地址不能变,但是地址上的值却是可变的

枚举类型(不用符号常量化)

1
2
3
4
5
6
7
8
9
10
11
enum name{name1[=num],name2[=num],name3,….};//编号从零开始,可以指定,后续自增
#include<stdio.h>
void f(enum color c) {
printf("%d", c);
}
int main(void) {
enum color{red = 1,yellow,blue};
enum color a = red;
f(a);
return 0;
}

结构体

定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct _a {
int a;
float b;
};
struct _a a;

struct _b {
int a;
float b;
}b;

struct {
int a;
float b;
}c;


赋值:
可单独赋值,也可整体赋值(使用{})
(注意:结构体变量名不是结构体变量的地址,所以才有p->next这种鬼)

typedef

C高阶的标志,所有类型重定义,略略略~

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
//#define BETAMAO 666666
int main(void) {
#ifdef BETAMAO
printf("betamao66666\n");
#else
printf("betamao23333\n");
#endif
return 0;
}

联合

1
#define MAX(x, y)(x>y ? x : y)

(注意最好不要加;不然容易死。。。)

位运算

输出一个数的二进制形式:

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

int main(void) {
int number;
scanf("%d", &number);
unsigned long mask = 1u<<31;//正宗的在32位上的1
for (; mask; mask >>= 1) {
printf("%d", number&mask ? 1 : 0);
}
return 0;
}

位段:

1
2
3
4
5
6
7
8
9
struct  weiduan
{
int image : 1;
int flag1 : 3;
int flag2 : 1;
int peding : 28;
}a;
a.image = 0;
a.flag1 = 3;

Windows基础

内核对象

C的结构体吧,存储在系统空间,使用对象句柄(HANDLE)访问,它的所有者是操作系统而非进程,用户只能通过操作系统提供的特定接口访问特定的对象。

对于进程,每个进程都有一个句柄表,句柄相当于索引

索引 指向内核对象内存块的指针 访问掩码 标志
1 0x00000000 (不可用) (不可用)
2 0x05000000 0x???????? 0x00000001

所以多个进程间不能直接使用句柄共享内核对象,默认句柄不会被子进程继承,可以通过创建时设置标志允许继承,当对对象命名后,在有权限的情况下,其他进程就可以通过对象名打开那个对象。

进程

进程是资源拥有的基本单位,上一篇已经记了,虚拟内存空间分布,其中用户空间属于进程独有,一般一个程序加载后布局:

另一部分,进程还有个进程内核对象:
进程控制块是进程实体的一部分,是操作系统中最重要的记录型数据结构。PCB中记录了操作系统所需的,用于描述进程进展情况及控制进程运行所需的全部信息。PCB是进程存在的惟一标志。一般把PCB存放在操作系统专门开辟的PCB区内。
在进程控制块中,主要包括下述4方面的信息:
(1)进程描述信息
进程标识符:每个进程都有惟一的进程标识符,用以识别不同的进程。
用户名或用户标识号:每个进程都隶属于某个用户,有利于资源共享与保护。
家族关系:标识进程之间的家族关系。

(2)处理机状态信息
通用寄存器、指令计数器、程序状态字(PSW)、用户栈指针等

(3)进程调度信息
进程状态:指明进程的当前状态,以作为进程调度和进程对换时的依据。
进程优先级:用于描述进程使用处理机的优先级别的一个整数,优先级别高的进先获得处理机。
进程调度所需的其他信息:如进程已等待CPU的时间总和、进程已执行的时间总和等。
事件:指进程被阻塞的原因。

(4)进程控制信息
程序和数据的地址:指出该进程的程序和数据所在的内存或外存地址,以便再调度到该进程执行时,能从中找到其程序和数据。
进程同步和通信机制:指实现进程同步和进程通信时所必须的机制,如消息队列指针、信号量等。这些数据应全部或部分地存放在PCB中。
实际上进程不会执行任何东西,它只是线程的容器(未使用到多线程时,也是进程的主线程执行整个程序),在C中,我们的程序是以_tmain/_tWinMain函数为入口点,但是实际编译时,在主函数之前还需要做一些初始工作,这部分代码由编译器添加,所以程序入口点地址(称作”入口函数吧”)并不是主函数地址。下面是一个程序的生命周期执行过程:

一旦用户登录一个系统后将拥有一个安全令牌(security token),以后访问受保护资源时都会使用这个令牌,令牌会传给子进程,一个进程拥有令牌后不能改变,若要更高权限的令牌也只能创建新进程并在那时申请更高权限令牌(UAC).

线程

与进程一样,线程也分两部分:在内核空间中的线程内核对象和用户空间中的线程栈
内核对象:
同进程,也记录着线程执行的一些信息,便于线程切换(用户态上下文和内核态的不一样)等:

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
79
80
81
82
83
84
85
typedef struct _CONTEXT {
//GetThreadContext()
//SetThreadContext()传入线程句柄与CONTEXT地址设置/获取上下文

DWORD ContextFlags;

//如果ContextFlags指定了CONTEXT_DEBUG_REGISTERS,这部分将被返回/指定,注意CONTEXT_DEBUG_REGISTERS不被包含在CONTEXT_FULL里
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;

//如果ContextFlags指定了CONTEXT_FLOATING_POINT,这部分将被返回/指定
FLOATING_SAVE_AREA FloatSave;

//如果ContextFlags指定了CONTEXT_SEGMENTS,这部分将被返回/指定
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;

//如果ContextFlags指定了CONTEXT_INTEGER,这部分将被返回/指定
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;

//如果ContextFlags指定了CONTEXT_CONTROL,这部分将被返回
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // 指定这两个要格外小心
DWORD EFlags;
DWORD Esp;
DWORD SegSs;

//如果ContextFlags指定了CONTEXT_EXTENDED_REGISTERS,这部分将被返回,这里的格式和上下文由处理器指定
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

typedef struct _CONTEXT {

//GetThreadContext()
//SetThreadContext()传入线程句柄与CONTEXT地址设置/获取上下文
DWORD ContextFlags;

//如果ContextFlags指定了CONTEXT_DEBUG_REGISTERS,这部分将被返回/指定,注意CONTEXT_DEBUG_REGISTERS不被包含在CONTEXT_FULL里
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;

//如果ContextFlags指定了CONTEXT_FLOATING_POINT,这部分将被返回/指定
FLOATING_SAVE_AREA FloatSave;

//如果ContextFlags指定了CONTEXT_SEGMENTS,这部分将被返回/指定
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;

//如果ContextFlags指定了CONTEXT_INTEGER,这部分将被返回/指定
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;

//如果ContextFlags指定了CONTEXT_CONTROL,这部分将被返回
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // 指定这两个要格外小心
DWORD EFlags;
DWORD Esp;
DWORD SegSs;

//如果ContextFlags指定了CONTEXT_EXTENDED_REGISTERS,这部分将被返回,这里的格式和上下文由处理器指定
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

线程栈:

创建线程后,会在用户空间申请一块区域存放线程局部数据(传入参数,局部变量都在这个栈中)
因为线程共享同一进程空间,所以线程是可以访问所属进程中所有的句柄,内存,其他线程的数据的
只有窗口和挂扣对象属于线程,其他用户对象都属于进程,所以若不显式关闭,线程结束时只有前两类句柄会被自动关闭(使用计数-1)

虚拟内存

虚拟内存

使用VirtualAlloc()使用MEM_REVERSE参数可以预定一块地址空间,此时它在虚拟内存里被预定但是并未被映射到物理存储,所以还是不能使用的,可以使用MEM_COMMIT参数调拨物理存储器空间,这样就能使用这块地址了。

内存映射文件

内存映射文件的物理存储器来自磁盘上已有的文件。例如运行exe文件,加载dll文件等,都是直接将文件映射到内存(这与CreateFile不一样,它只是打开文件,可以通过文件句柄,偏移指针访问文件内容,而内存映射是可以完全映射内容的),当同一程序被同时多次执行,它只会被映射一次,其他进程只需复制它的映射视图即可,不过默认情况下他们的全局变量和静态变量不会被共享,而且当一个进程修改了其他部分(如hook API修改了代码)会发生写时复制。

对这种神奇的东西细节留到堆溢出部分来记录,现在先记下大概的使用。它是一块预定的地址空间区域,物理存储器始终是页交换文件,进程初始化时会被分配一个默认堆,可使用GetProcessHeap()获取句柄,用户可以自己创建HeapCreate()堆取,然后从定义的堆取里面分配HeapAlloc()内存

DLL

隐式调用

在编写程序时,只包含dll的头文件,其中头文件会声明导出数据结构,符号,函数和变量,它需要用到*.lib,生成的可执行文件IAT中会有各种需要导入的dll数据,在程序运行前dll就会被加载到内存中。

显示调用

在编写程序时,包含头文件并使用LoadLibrary()显式加载dll,加载的dll不会出现在IAT中,只有程序运行到函数出才会动态加载dll并在使用完以后可以通过FreeLibrary卸载,在进程/线程对此dll进行装载/卸载时可以执行指定的初始化/退出代码(具体笔记在消息勾取处)