dustland

dustball in dustland

虚拟硬盘vhd

virtual hard disk

用virtualbox新建的没有操作系统的虚拟电脑,选择VHD格式虚拟硬盘,固定大小2G.

刚建立的vhd虚拟硬盘文件的最后512字节是有意义的,前面其他字节都是空的

这最后512个字节是啥呢?

image-20220825094005879

结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct hd_ftr { //harddisk_footer
  char   cookie[8];       /* Identifies original creator of the disk      */ //魔数
  u32    features;        /* Feature Support -- see below                 */ 
  u32    ff_version;      /* (major,minor) version of disk file           */ 
  u64    data_offset;     /* Abs. offset from SOF to next structure       */ 
  u32    timestamp;       /* Creation time.  secs since 1/1/2000GMT       */ 
  char   crtr_app[4];     /* Creator application                          */ 
  u32    crtr_ver;        /* Creator version (major,minor)                */ 
  u32    crtr_os;         /* Creator host OS                              */ 
  u64    orig_size;       /* Size at creation (bytes)                     */ 
  u64    curr_size;       /* Current size of disk (bytes)                 */ 
  u32    geometry;        /* Disk geometry                                */ 
  u32    type;            /* Disk type                                    */ 
  u32    checksum;        /* 1's comp sum of this struct.                 */ 
  vhd_uuid_t uuid;        /* Unique disk ID, used for naming parents      */ 
  char   saved;           /* one-bit -- is this disk/VM in a saved state? */ 
  char   hidden;          /* tapdisk-specific field: is this vdi hidden?  */ 
  char   reserved[426];   /* padding                                      */ 
}; 

可以理解为文件魔数,通过检查cookie值是否是"conectix",判断该VHD是否有效

conectix是connectix公司的名称缩写,这个技术是connectix发明的

features

1
2
3
#define HD_NO_FEATURES     0x00000000 
#define HD_TEMPORARY       0x00000001 /* disk can be deleted on shutdown */ //只要关机就删除硬盘
#define HD_RESERVED        0x00000002 /* NOTE: must always be set        */ 

ff_version

VHD版本

data_offset

对固定磁盘来说,该值总是0xFFFFFFFF,不重要

对于动态磁盘来说,该值是dd_hdr的文件偏移量

timestamp

VHD文件的创建时间,不重要

crtr_app

创建该VHD文件使用的应用程序,如果是virtualbox创建的则为"vbox"

crtr_ver

创建版本,不同磁盘版本管理方法不同

crtr_os

操作系统枚举值

image-20220825094803757

orig_size

创建时该虚拟磁盘的可用寻址空间,大小等于整个vhd文件大小减去该尾部扇区512字节

该nobody.vhd文件在创建时我们选择的大小是整2G,用010editor打开观察,发现除了最后一个扇区正好2G,全空.

加上最后一个扇区则为2GB+512B

curr_size

当前大小,可能是扩容后的可用寻址空间

geometry

几何参数,即柱面数C,磁头数H,扇区数S

image-20220825102119209

VHD规范中,每个扇区sector大小是512B

刚建立的2G的虚拟硬盘,有4161个柱面

16个磁头,即16个盘面

每个柱面上有63个扇区

VHD文件最开始的512字节,就对磁盘的0面0道1扇区,也就是主引导扇区

接下来是0面0道2扇区

0面0道3扇区

...

0面0道63扇区

1面0道1扇区

1面0道2扇区

...

1面0道63扇区

...

15面0道0扇区

15面0道1扇区

15面0道63扇区

0面1道0扇区

type

VHD类型

1
2
3
4
#define HD_TYPE_NONE       0 //无类型
#define HD_TYPE_FIXED      2 //固定大小 
#define HD_TYPE_DYNAMIC    3 //动态大小
#define HD_TYPE_DIFF       4 //差分磁盘

前三个容易理解,第四个差分磁盘是什么呢?

差分盘分成母盘和子盘,一个母盘可以带好几个子盘,母盘存放这几个子盘共有的东西,比如操作系统代码,就跟so动态库似的可以复用

每个子盘中单独存放自己的数据

假如原来要装四个虚拟机,每个虚拟机操作系统在磁盘中占8g,剩下2g磁盘存放数据.

如果不使用差分磁盘,则四个虚拟机就实打实地需要40G的物理磁盘

然而每个虚拟机中占据8G磁盘的操作系统实际上是搞重复建设

因此可以用母盘放8G的操作系统,四个子盘每个2G存放各自的数据.

这样总共占用物理磁盘16G

checksum

检校和

最后这个扇区的所有字节相加得到的32位数按位取反

uuid

VHD识别号,决定VHD主从关系

saved

动态磁盘才会使用,不重要

reserved

保留关键字,尚未使用

物理磁盘

磁盘结构

结构总览

img
image-20220825105415000

面0,面1,实际上就是磁头0,磁头1

因为每个面上都会有一个磁头负责读写该面的数据

扇区和磁道的关系

img

一个盘面从里向外有多个同心圆磁道,每个磁道上有多个扇区,但是任意两个磁道的扇区数是相同的,

可以想象,越往外的磁道,一个扇区约长,数据约稀疏

磁头和柱面

img

柱面和磁头都是从0开始编号,但是扇区是从1开始编号的

一个盘片就像一个烧饼一样有两面,每面上都有磁道,一个柱面就是所有等半径的磁道集合

比如有三个盘片就有6个盘面,那么半径为2厘米的磁道就有6条,半径为2厘米的柱面就包含了这6条磁道

有多少个盘面就得有多少个磁头,每个磁头负责一个盘面的读写

所有磁头的动作都是同步的,就算是要读写最顶上这个盘的上面,所有的磁头都得跟着最上面这个磁头动,从顶向下看所有磁头是重叠的.但是只有上面这个磁头可以读写数据.控制哪个磁头也是硬盘控制器要做的工作

读写速度

磁盘的读写速度有三个方面组成

寻道时间,旋转时间,传送时间

寻道时间

假如一开始磁头都趴在最外环上,现在需要往最内环的扇区读写数据,那么传动臂就得跟个塔吊一样把磁头吊过去

这是肉眼可见的,显然速度非常慢

旋转时间

现在磁头已经到达了最内环,要读写该环上的第一个扇区,但是不巧,磁头刚过来时,第一个扇区刚转过去,磁头就得等到盘子转一整圈才能盼来第一个扇区,而CPU早已望眼欲穿了

传送时间

扇区也到位了,磁头赶紧趴下看看扇区上写了啥

这个过程肉眼几乎不可见,是最快的

这三个过程前面两个都是毫秒量级的,并且差不多.最后的传送时间稀松了了,相对于前面俩可以忽略

访问模式

访问模式有两种,老古董CHS模式,即通过指定柱面,磁头,扇区三个坐标访问一个扇区

现代LBA(logical block address)模式,不考虑柱面,磁头数了,所有扇区统一编号,将逻辑扇区号翻译成CHS的工作交给硬盘控制器干

LBA和CHS的映射关系

image-20220825111708551

逻辑扇区从0开始编号

假设某个硬盘有h个磁头,c个柱面,每个磁道上有s个扇区,给定一个逻辑扇区号g,求改扇区落在哪个盘子的哪个磁道上

这三个单位的权重是c>h>s

因为寻道时间>旋转时间>传送时间,因此最忌讳动传动臂,磁头最好老实在固定的一环上趴着,等这个柱面上的所有磁道都满了,才迫不得已动一下传动臂换个圈转

这就是为啥权重c>h

每个柱面上有h*s个扇区,每个磁道上有s个扇区

n*h*s<=g<(n+1)*h*s

n就是第几个柱面,也就是哪个磁道,至于哪个盘子

1
k*s<=(g-n*h*s)<(k+1)*s

k就是哪个盘子

瞎写主引导记录

程序来自教材x86汇编语言从实模式到保护模式

image-20220825113210639

他这段程序用nasm编会报错,因为mov [0x00],'a'只指定了目的地址,但是没有说明大小

我是把'a'搬到0x00这个位置上一个字节?还是0x00~0x01这个字还是0x00~0x03这个双字?没有说清.

假:'a'不是一个字节吗?自然搬过去还是一个字节喽!

这里'a'实际上就是一个立即数,就是'a'的ASCII码.而一个立即数是不能决定占用几个字节的

如果写mov [0x00],al这样就可以,把al这个字节寄存器的东西搬到内存0x00位置上,这时候就不用指定多大的内存了,因为al自己隐含就是一个字节寄存器了

并且这一点儿代码根本凑不够一个扇区,需要写一些废话填满一个扇区才有效

应该这样写:

1
2
3
4
5
6
7
8
9
10
11
; 4-2.asm
mov ax,0xb800
mov ds,ax

mov byte [0x00],'a'
mov byte [0x02],'s'
mov byte [0x04],'m'

jmp $
times 510-($-$$) db 0
db 0x55,0xaa

编译:

1
nasm main.asm -o main.bin

然后使用fixvhdw2把main.bin写到虚拟磁盘nobody.vhd中

image-20220825115728499

现在就可以开启虚拟机了

image-20220825115716225

asm三个字已经打印在最左上角

如果不够512字节

如果main.asm没写废话

1
2
3
4
5
6
mov ax,0xb800                 
mov ds,ax

mov byte [0x00],'a'
mov byte [0x02],'s'
mov byte [0x04],'m'

编译成main.bin之后是远没有512个字节的

image-20220825115945064

不够512个字节的主引导记录作废了,写到磁盘里然后尝试启动会说找不到靴子

至于怎么凑够的512字节,以及为啥可以在屏幕上输出asm,那是后话了

windows SDK chapter 11 对话框

模态对话框

模态对话框指对话框存在时不能操作父窗口,必须关闭模态对话框才能和父窗口进行交互

如果是普通的模态对话框,此时还可以到其他应用程序.

如果是系统的模态对话框,则整个操作系统都会被这一个模态对话框锁住,必须关了系统模态对话框才可以干别的

因为父窗口过程直接将控制转移给了对话框函数,啥时候对话框函数返回即对话框关闭,控制才能还给父窗口

About1

主窗口过程WndProc在处理菜单消息时有一个创建对话框的情况,即点选ID_APP_ABOUT对应的菜单

1
2
3
4
5
6
7
8
case WM_COMMAND :
switch (LOWORD (wParam))
{
case IDM_APP_ABOUT :
DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc) ;//创建对话框,使用AboutBox资源
break ;
}
return 0 ;
1
2
3
4
5
6
void DialogBoxA(
[in, optional] hInstance,//exe句柄
[in] lpTemplate,//对话框模板,可以加载资源
[in, optional] hWndParent,//父窗口句柄
[in, optional] lpDialogFunc//对话框过程函数
);

该函数会一直等到lpDialogFunc回调函数中调用EndDialog才会结束执行,将控制还给WndProc,

这意味在对话框起来之后,主窗口啥也干不了,除非把对话框扬了.

创建对话框的过程:

DialogBox宏用CreateWindowEx函数创建对话框。DialogBox函数然后把一个WM_INITDIALOG消息(和一个WM_SETFONT消息,如果模板指定DS_SETFONT类型)传递到对话框过程。不管模板是否指定WS_VISIBLE类型,函数显示对话框,并且使拥有该对话框的窗口(也称属主窗口)失效,且为对话框启动它本身的消息循环来检索和传递消息。

当对话框应用程序调用EndDialog函数时,DialogBox函数清除对话框户止消息循环,使属主窗口生效(如果以前有效),且返回函数EndDialog调用中的nReSUlt参数。

对话框过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message, 
WPARAM wParam, LPARAM lParam)//hDlg持有自己的对话框句柄
{
switch (message)
{
case WM_INITDIALOG :
return TRUE ;

case WM_COMMAND :
switch (LOWORD (wParam))
{
case IDOK :
case IDCANCEL ://然而该对话框没有取消按钮,只有一个OK按钮,可以使用Esc键触发IDCANCLE
EndDialog (hDlg, 0) ;//结束对话框
return TRUE ;
}
break ;
}
return FALSE ;
}

1
2
3
4
BOOL EndDialog(
[in] HWND hDlg,//需要结束的对话框句柄
[in] INT_PTR nResult//返回值,主窗口可以根据该值与对话框进行交互
);

使用DialogBox创建的对话框必须使用EndDialog制死

EndDialog函数是在对话框窗口过程中调用的,并且只能用于结束对话框过程

EndDialog中指定的返回值就是DialogBox函数的返回值

设计对话框

visual studio添加对话框资源,可以交互式设计对话框

rc文件中

1
2
3
4
5
6
7
8
9
10
ABOUTBOX DIALOGEX 32, 32, 180, 102		//名字(用于索引该对话框) 对话框类型 左上角横坐标,左上角纵坐标,宽度,高度
STYLE DS_SETFONT | WS_POPUP | WS_THICKFRAME //风格 对话框有字,弹出,薄框架
FONT 8, "MS Sans Serif", 0, 0, 0x0 //字号8 字体 ,balabala
BEGIN //对话框主题开始
DEFPUSHBUTTON "OK",IDOK,66,81,50,14 //一个默认按钮,ID是IDOK,上有字样"OK" 左上角(66,81) 宽50,高14
ICON "ABOUT1",IDC_STATIC,7,7,20,20 //图标,名称"ABOUT1" ID是IDC_STATIC,左上角(7,7),宽高20(对于ICON来说该宽高无意义)
CTEXT "dialog",IDC_STATIC,40,12,100,8 //文字
CTEXT "dustball",IDC_STATIC,7,40,166,8
CTEXT "(c) dustball, 2022",IDC_STATIC,7,52,166,8
END //对话框主题结束

这里所有数值的单位都不是像素,而是对话框所用字体的宽的四分之一高的八分之一,具体怎么算的不重要,交互式设计不需要闷头算他娘的

BEGIN和END之间是对话框子窗口控件

1
控件类型 "文本",id,左坐标,上坐标,宽度,高度,控件风格

控件风格比如WS_CHILD,SS_CENTER,WS_VISIBLE

对话框过程

1
BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message,WPARAM wParam, LPARAM lParam);

一个返回真假的回调函数,有四个参数,对话框句柄,消息,w参和l参

对比窗口过程和对话框过程

对话框过程 窗口过程
返回值 BOOL LRESULT
DefWindowProc 不会调用,只会返回TRUE 处理不了的消息调用DefWindowProc
处理消息 不需要处理WM_PAINT,WM_DESTORY
不会收到WM_CREATE,
有专门消息WM_INITDIALOG
只需要处理WM_COMMAND

当对话框中按下按钮时,这个按钮回向它的父窗口,也就是这个对话框,发送WM_COMMAND消息,wParam是控件ID

About2

资源脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ABOUTBOX DIALOG DISCARDABLE  32, 32, 200, 234	//名叫ABOUTBOX
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION
FONT 8, "MS Sans Serif"
BEGIN
ICON "ABOUT2",IDC_STATIC,7,7,20,20
CTEXT "About2",IDC_STATIC,57,12,86,8
CTEXT "About Box Demo Program",IDC_STATIC,7,40,186,8
LTEXT "",IDC_PAINT,114,67,72,72 //预览块
GROUPBOX "&Color",IDC_STATIC,7,60,84,143
RADIOBUTTON "&Black",IDC_BLACK,16,76,64,8,WS_GROUP | WS_TABSTOP//单选组开始,tab键停留位置,使用tab键可以更换当前对话框内的焦点.同一组内按钮可以使用上下左右键调整选择
RADIOBUTTON "B&lue",IDC_BLUE,16,92,64,8
RADIOBUTTON "&Green",IDC_GREEN,16,108,64,8
RADIOBUTTON "Cya&n",IDC_CYAN,16,124,64,8
RADIOBUTTON "&Red",IDC_RED,16,140,64,8
RADIOBUTTON "&Magenta",IDC_MAGENTA,16,156,64,8
RADIOBUTTON "&Yellow",IDC_YELLOW,16,172,64,8
RADIOBUTTON "&White",IDC_WHITE,16,188,64,8
GROUPBOX "&Figure",IDC_STATIC,109,156,84,46,WS_GROUP
RADIOBUTTON "Rec&tangle",IDC_RECT,116,172,65,8,WS_GROUP | WS_TABSTOP
RADIOBUTTON "&Ellipse",IDC_ELLIPSE,116,188,64,8
DEFPUSHBUTTON "OK",IDOK,35,212,50,14,WS_GROUP
PUSHBUTTON "Cancel",IDCANCEL,113,212,50,14,WS_GROUP
END

全局变量

1
2
int iCurrentColor = IDC_BLACK;  //当前颜色
int iCurrentFigure = IDC_RECT ;//当前形状

这两个变量决定着预览框和主窗口如何绘制.

通过对话框修改

父窗口过程

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
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HINSTANCE hInstance ;
PAINTSTRUCT ps ;

switch (message)
{
case WM_CREATE:
hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;
return 0 ;

case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDM_APP_ABOUT:
if (DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc))
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
}
break ;

case WM_PAINT:
BeginPaint (hwnd, &ps) ;
EndPaint (hwnd, &ps) ;

PaintWindow (hwnd, iCurrentColor, iCurrentFigure) ;
return 0 ;

case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

PaintWindow

使用iColor颜色和iFigure形状绘制hwnd指向的窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void PaintWindow (HWND hwnd, int iColor, int iFigure)//根据指定颜色和形状绘制窗口
{
static COLORREF crColor[8] = { RGB ( 0, 0, 0), RGB ( 0, 0, 255),
RGB ( 0, 255, 0), RGB ( 0, 255, 255),
RGB (255, 0, 0), RGB (255, 0, 255),
RGB (255, 255, 0), RGB (255, 255, 255) } ;//颜色列表

HBRUSH hBrush ;
HDC hdc ;
RECT rect ;

hdc = GetDC (hwnd) ;
GetClientRect (hwnd, &rect) ;
hBrush = CreateSolidBrush (crColor[iColor - IDC_BLACK]) ;//iColor是当前正在使用的颜色
hBrush = (HBRUSH) SelectObject (hdc, hBrush) ;

if (iFigure == IDC_RECT)//绘制矩形
Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ;
else//绘制扁瓜蛋
Ellipse (hdc, rect.left, rect.top, rect.right, rect.bottom) ;

DeleteObject (SelectObject (hdc, hBrush)) ;
ReleaseDC (hwnd, hdc) ;
}

对话框过程

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

BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam)
{
static HWND hCtrlBlock ;//预览块句柄
static int iColor, iFigure ;

switch (message)
{
case WM_INITDIALOG:
iColor = iCurrentColor ;//获取当前颜色
iFigure = iCurrentFigure ;

CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE, iColor) ;//第一个颜色和最后一个颜色,其间的宏定义都算,这一些选项中有且只有一个处于被选中状态
CheckRadioButton (hDlg, IDC_RECT, IDC_ELLIPSE, iFigure) ;

hCtrlBlock = GetDlgItem (hDlg, IDC_PAINT) ;//获取预览块句柄

SetFocus (GetDlgItem (hDlg, iColor)) ;//iColor就是选中颜色按钮的句柄,将焦点让给该按钮
return FALSE ;

case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDOK://点选了确认键,需要修改设置了
iCurrentColor = iColor ;
iCurrentFigure = iFigure ;//修改当前设置
EndDialog (hDlg, TRUE) ;
return TRUE ;

case IDCANCEL:
EndDialog (hDlg, FALSE) ;
return TRUE ;

case IDC_BLACK:
case IDC_RED:
case IDC_GREEN:
case IDC_YELLOW:
case IDC_BLUE:
case IDC_MAGENTA:
case IDC_CYAN:
case IDC_WHITE:
iColor = LOWORD (wParam) ;//LOWORD(wParam)携带控件ID
CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE, LOWORD (wParam)) ;//根据鼠标点选改变单选按钮状态
PaintTheBlock (hCtrlBlock, iColor, iFigure) ;//Block是预览块,实时根据选择改变,提供预览功能
return TRUE ;

case IDC_RECT:
case IDC_ELLIPSE:
iFigure = LOWORD (wParam) ;//更新图形状态
CheckRadioButton (hDlg, IDC_RECT, IDC_ELLIPSE, LOWORD (wParam)) ;
PaintTheBlock (hCtrlBlock, iColor, iFigure) ;//立刻重绘预览块
return TRUE ;
}
break ;

case WM_PAINT:
PaintTheBlock (hCtrlBlock, iColor, iFigure) ;//重绘时只需要关注预览块
break ;
}
return FALSE ;
}

PaintTheBlock

1
2
3
4
5
6
void PaintTheBlock (HWND hCtrl, int iColor, int iFigure)
{
InvalidateRect (hCtrl, NULL, TRUE) ;//hCtrl句柄控制的客户去全都失效
UpdateWindow (hCtrl) ;//重绘hCtrl
PaintWindow (hCtrl, iColor, iFigure) ;//根据hCtrl和iColor指定的风格重绘
}

回顾父子通信方式

父窗口就只负责主窗口客户区的颜色和图形绘制,对话框负责调这个颜色和图形

父窗口和对话框如何进行通信的?或者说对话框是如何通知父窗口用新样式绘图的呢?

关键在于两个对父过程和对话框过程都可见的全局变量,对话框使用这两个变量间接通知父窗口

并且在对话框关闭之后,父窗口会立刻重绘,这就使得颜色样式更新显得没有延迟

父窗口中,关闭对话框之后的立刻更新:

1
2
3
case IDM_APP_ABOUT:
if (DialogBox (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc))//如果DialogBox返回值1说明确实有修改,需要重绘,否则不需要重绘
InvalidateRect (hwnd, NULL, TRUE) ;

避免使用全局变量

正当我感觉使用全局变量间接实现父子通信,这个设计合情合理,甚至有点巧妙时,人家又说,这是low B方法.

书上给的方法是,定义一个结构体

1
2
3
typedef struct {
int iColor, iFigure;
}ABOUTBOX_DATA;

这个结构体就记录了对话框能够使用的变量

在WndProc中用这样一个结构体指针传递给DialogBoxParam,即带参数的对话框函数

1
2
3
4
5
6
7
INT_PTR DialogBoxParamA(
[in, optional] HINSTANCE hInstance,//应用程序实例句柄
[in] LPCSTR lpTemplateName,//对话框id
[in, optional] HWND hWndParent,//父窗口
[in, optional] DLGPROC lpDialogFunc,//对话框过程
[in] LPARAM dwInitParam//参数
);

dwInitParam这个参数将会作为对话框WM_INITDIALOG消息的lParam参数传递给对话框过程,也就是把WndProc函数栈变量的地址交给了对话框过程AboutDlgProc,使得对话框过程函数可以操作WndProc的函数栈

AboutDlgProc中有两个关于ABOUTBOX_DATA结构体的变量,一个是指针类型,保管WndProc用WM_INITDIALOG消息的lParam参数指定的指针,用于向WndProc打报告

另一个是AboutDlgProc函数栈下的局部变量,这个是对话框过程自娱自乐用的,打报告的时候只需要拷贝该局部变量的情况

修改后的About2

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/*------------------------------------------
ABOUT2.C -- About Box Demo Program No. 2
(c) Charles Petzold, 1998
------------------------------------------*/

#include <windows.h>
#include "resource.h"

typedef struct {//结构体定义
int iColor, iFigure;
}ABOUTBOX_DATA;


LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
BOOL CALLBACK AboutDlgProc (HWND, UINT, WPARAM, LPARAM) ;


int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("About2") ;
MSG msg ;
HWND hwnd ;
WNDCLASS wndclass ;

wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (hInstance, szAppName) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = szAppName ;
wndclass.lpszClassName = szAppName ;

if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}

hwnd = CreateWindow (szAppName, TEXT ("About Box Demo Program"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;

ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;

while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}

void PaintWindow (HWND hwnd, int iColor, int iFigure)
{
static COLORREF crColor[8] = { RGB ( 0, 0, 0), RGB ( 0, 0, 255),
RGB ( 0, 255, 0), RGB ( 0, 255, 255),
RGB (255, 0, 0), RGB (255, 0, 255),
RGB (255, 255, 0), RGB (255, 255, 255) } ;

HBRUSH hBrush ;
HDC hdc ;
RECT rect ;

hdc = GetDC (hwnd) ;
GetClientRect (hwnd, &rect) ;
hBrush = CreateSolidBrush (crColor[iColor - IDC_BLACK]) ;
hBrush = (HBRUSH) SelectObject (hdc, hBrush) ;

if (iFigure == IDC_RECT)
Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ;
else
Ellipse (hdc, rect.left, rect.top, rect.right, rect.bottom) ;

DeleteObject (SelectObject (hdc, hBrush)) ;
ReleaseDC (hwnd, hdc) ;
}

void PaintTheBlock (HWND hCtrl, int iColor, int iFigure)
{
InvalidateRect (hCtrl, NULL, TRUE) ;
UpdateWindow (hCtrl) ;
PaintWindow (hCtrl, iColor, iFigure) ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HINSTANCE hInstance ;//应用程序实例句柄
static PAINTSTRUCT ps ;
static ABOUTBOX_DATA ad;//父窗口绘图依据

switch (message)
{
case WM_CREATE:
hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;
return 0 ;

case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDM_APP_ABOUT:
if (DialogBoxParam (hInstance, TEXT ("AboutBox"), hwnd, AboutDlgProc,&ad))//ad地址交给子窗口,使其可以跨函数栈帧修改ad值
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
}
break ;

case WM_PAINT:
BeginPaint (hwnd, &ps) ;
EndPaint (hwnd, &ps) ;

PaintWindow (hwnd, ad.iColor, ad.iFigure) ;//根据当前颜色当前形状绘图
return 0 ;

case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

BOOL CALLBACK AboutDlgProc (HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam)
{
static HWND hCtrlBlock ;
static ABOUTBOX_DATA ad,*pad;
switch (message)
{
case WM_INITDIALOG:
pad = (ABOUTBOX_DATA*)lParam;//pad用于承接父窗口传递的指针
ad = *pad;//ad是本函数绘图依据,不到万不得已不使用*pad指针


CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE, ad.iColor) ;//第一个颜色和最后一个颜色,其间的宏定义都算,这一些选项中有且只有一个处于被选中状态
CheckRadioButton (hDlg, IDC_RECT, IDC_ELLIPSE, ad.iFigure) ;

hCtrlBlock = GetDlgItem (hDlg, IDC_PAINT) ;//预览块矿建

SetFocus (GetDlgItem (hDlg, ad.iColor)) ;
return FALSE ;

case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDOK://点选了确认键,需要修改设置了
*pad = ad;
EndDialog (hDlg, TRUE) ;
return TRUE ;

case IDCANCEL:
EndDialog (hDlg, FALSE) ;
return TRUE ;

case IDC_BLACK:
case IDC_RED:
case IDC_GREEN:
case IDC_YELLOW:
case IDC_BLUE:
case IDC_MAGENTA:
case IDC_CYAN:
case IDC_WHITE:
ad.iColor = LOWORD (wParam) ;//LOWORD(wParam)携带控件ID
CheckRadioButton (hDlg, IDC_BLACK, IDC_WHITE, LOWORD (wParam)) ;
PaintTheBlock (hCtrlBlock, ad.iColor,ad.iFigure ) ;//Block是预览块,实时根据选择改变,提供预览功能
return TRUE ;

case IDC_RECT:
case IDC_ELLIPSE:
ad.iFigure = LOWORD (wParam) ;
CheckRadioButton (hDlg, IDC_RECT, IDC_ELLIPSE, LOWORD (wParam)) ;
PaintTheBlock (hCtrlBlock, ad.iColor, ad.iFigure) ;
return TRUE ;
}
break ;

case WM_PAINT:
PaintTheBlock (hCtrlBlock, ad.iColor, ad.iFigure) ;
break ;
}
return FALSE ;
}

回顾单选互斥的实现

之前实现单选互斥,需要维护一组按钮的状态,当新按钮按下之前,先得把原来按下的按钮扣起来,在按下新按钮.

而现在只需要

1
CheckRadioButton(hDlg,idFirst,idLast,idCheck);

意思是,在hDlg对话框中,编号属于[idFirst,idLast]这个范围的单选按钮都是互斥的,idCheck决定按下哪一个.

这就要求从idFirst到idLast是连号的,idCheck属于这个范围

About3

自定义对话框控件

资源脚本

1
2
3
4
5
6
7
8
9
10
ABOUTBOX DIALOG DISCARDABLE  32, 32, 180, 100
STYLE DS_MODALFRAME | WS_POPUP
FONT 8, "MS Sans Serif"
BEGIN
CONTROL "OK",IDOK,"EllipPush",WS_GROUP | WS_TABSTOP,73,79,32,14//自定义控件,ID=IDOK,类名="EllipPush"
ICON "ABOUT3",IDC_STATIC,7,7,20,20
CTEXT "About3",IDC_STATIC,40,12,100,8
CTEXT "About Box Demo Program",IDC_STATIC,7,40,166,8
CTEXT "(c) Charles Petzold, 1998",IDC_STATIC,7,52,166,8
END

这里的类名就是注册窗口类时的类名,决定从该类创建的实例的属性

自定义控件过程函数

创建自定义控件类的时候需要注册其过程函数,其中WM_PAINT决定了该自定义控件类的外观

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
LRESULT CALLBACK EllipPushWndProc (HWND hwnd, UINT message, //自定义按钮类行为
WPARAM wParam, LPARAM lParam)
{
TCHAR szText[40] ;
HBRUSH hBrush ;
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;

switch (message)
{
case WM_PAINT ://绘图行为
GetClientRect (hwnd, &rect) ;
GetWindowText (hwnd, szText, lstrlen(szText)) ;//获取本子窗口的描述文字,写到szText数组里,即"OK"字样

hdc = BeginPaint (hwnd, &ps) ;

hBrush = CreateSolidBrush (GetSysColor (COLOR_WINDOW)) ;
hBrush = (HBRUSH) SelectObject (hdc, hBrush) ;
SetBkColor (hdc, GetSysColor (COLOR_WINDOW)) ;
SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT)) ;

Ellipse (hdc, rect.left, rect.top, rect.right, rect.bottom) ;//绘制椭圆
DrawText (hdc, szText, -1, &rect,//椭圆中心写字
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

DeleteObject (SelectObject (hdc, hBrush)) ;

EndPaint (hwnd, &ps) ;
return 0 ;

case WM_KEYUP :
if (wParam != VK_SPACE)//按下空格键相当于按下鼠标左键,其他键忽略,交给DefWindowProc处理
break ;
// fall through
case WM_LBUTTONUP :
SendMessage (GetParent (hwnd), WM_COMMAND,//向父窗口即对话框发送WM_COMMAND消息,消息内容是本按钮的id
GetWindowLong (hwnd, GWL_ID), (LPARAM) hwnd) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

非模态对话框

非模态对话框就是可以不理会对话框内容继续和父窗口交互

一般Ctrl+F召唤的查找框就是非模态的

使用CreateDialogA函数创建非模态对话框

1
2
3
4
5
6
void CreateDialogA(
[in, optional] hInstance,
[in] lpName,//对话框类名,或者类句柄
[in, optional] hWndParent,//父窗口句柄
[in, optional] lpDialogFunc//对话框回调函数
);

COLORS

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL CALLBACK ColorScrDlg(HWND, UINT, WPARAM, LPARAM);

HWND hDlgModeless; //非模态对话框的全局句柄,对任意函数可见,

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Colors2");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;

wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = CreateSolidBrush(0L);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;

if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}

hwnd = CreateWindow(
szAppName, TEXT("Color Scroll"),
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,//不擦除子窗口的情况下,重绘父窗口
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL
);

ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);

hDlgModeless = CreateDialog(hInstance, TEXT("ColorScrDlg"), hwnd, ColorScrDlg);//在winMain中创建非模态对话框,使用字符串索引资源,注册非模态对话框过程ColorScrDlg


while (GetMessage(&msg, NULL, 0, 0))
{
if (hDlgModeless == 0 || !IsDialogMessage(hDlgModeless, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
DeleteObject((HGDIOBJ)SetClassLong(hwnd, GCL_HBRBACKGROUND,(LONG)GetStockObject(WHITE_BRUSH)));//删除注册窗口类时创建的逻辑画刷,避免内存泄漏
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}

BOOL CALLBACK ColorScrDlg(HWND hDlg, UINT message,
WPARAM wParam, LPARAM lParam)//对话框过程回调
{
static int iColor[3];
HWND hwndParent, hCtrl;
int iCtrlID, iIndex;

switch (message)
{
case WM_INITDIALOG://初始化对话框
for (iCtrlID = 10; iCtrlID < 13; iCtrlID++)//三个滚动条的ID分别是10,11,12
{
hCtrl = GetDlgItem(hDlg, iCtrlID);//获取hDlg句柄对应的对话框上,ID是iCtrlID的控件的句柄,即滚动条控件的句柄
SetScrollRange(hCtrl, SB_CTL, 0, 255, FALSE);//设置滚动条范围0到255,恰好是RGB值的范围
SetScrollPos(hCtrl, SB_CTL, 0, FALSE);//设置滑块初始位置在0上
}
return TRUE;

case WM_VSCROLL:
hCtrl = (HWND)lParam;//是三个滚动条中的哪一个产生的消息
iCtrlID = GetWindowLong(hCtrl, GWL_ID);//获取id
iIndex = iCtrlID - 10;//转换成0,1,2下标//该下标用于索引iColor数组,iColor[0]表示红色色度条//同时iColor[0]值也表示了该条上的滑块位置
hwndParent = GetParent(hDlg);//获取父窗口也就是主窗口句柄

switch (LOWORD(wParam))
{
case SB_PAGEDOWN:
iColor[iIndex] += 15; // fall through//一页是滚动15+1个单位,但是最大不能超过255个单位
case SB_LINEDOWN:
iColor[iIndex] = min(255, iColor[iIndex] + 1);
break;
case SB_PAGEUP:
iColor[iIndex] -= 15; // fall through
case SB_LINEUP:
iColor[iIndex] = max(0, iColor[iIndex] - 1);
break;
case SB_TOP:
iColor[iIndex] = 0;
break;
case SB_BOTTOM:
iColor[iIndex] = 255;
break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK://拖动滑块
iColor[iIndex] = HIWORD(wParam);
break;
default:
return FALSE;
}
SetScrollPos(hCtrl, SB_CTL, iColor[iIndex], TRUE);//设置滑块位置
SetDlgItemInt(hDlg, iCtrlID + 3, iColor[iIndex], FALSE);//设置对话框控件文本,也就是表明当前色度的数值
//+3是将滚动条号转化为其上的解释性文字的id

DeleteObject((HGDIOBJ)SetClassLong(hwndParent, GCL_HBRBACKGROUND,//删除旧背景颜色
(LONG)CreateSolidBrush(RGB(iColor[0], iColor[1], iColor[2]))));//更新新背景颜色

InvalidateRect(hwndParent, NULL, TRUE);//父窗口全部失效,立刻通知重绘
return TRUE;
}
return FALSE;
}

对话框资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

COLORSCRDLG DIALOG DISCARDABLE 16, 16, 120, 141 //对话框名叫"COLORSCRDLG"
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION //非模态,弹出,可见,有标题
CAPTION "Color Scroll Scrollbars" //对话框标题
FONT 8, "MS Sans Serif" //字号字体
BEGIN
CTEXT "&Red",IDC_STATIC,8,8,24,8,NOT WS_GROUP //滚动条两头的解释性文字,没有逻辑作用
SCROLLBAR 10,8,20,24,100,SBS_VERT | WS_TABSTOP //滚动条, 数值方向滚动,制表停留
CTEXT "0",13,8,124,24,8,NOT WS_GROUP
CTEXT "&Green",IDC_STATIC,48,8,24,8,NOT WS_GROUP
SCROLLBAR 11,48,20,24,100,SBS_VERT | WS_TABSTOP
CTEXT "0",14,48,124,24,8,NOT WS_GROUP
CTEXT "&Blue",IDC_STATIC,89,8,24,8,NOT WS_GROUP
SCROLLBAR 12,89,20,24,100,SBS_VERT | WS_TABSTOP
CTEXT "0",15,89,124,24,8,NOT WS_GROUP
END

CreateDialog和DialogBox

CreateDialog创建非模态对话框的参数和DialogBox创建模态对话框的参数一模一样

两个函数的区别是是否持有控制

DialogBox调用之后控制会从父窗口过程函数转移给对话框过程函数,返回值是一个数

但是CreateDialog会立刻返回对话框的句柄,不会持有控制.

之所以CreateDialog需要返回句柄,是因为非模态对话框的父窗口是可以动的,它可能需要使用对话框句柄

新消息循环

非模态对话框的消息要进入WinMain中的消息循环,相当于一个子窗口,需要WinMain消息循环分拣派发消息

但是模态对话框的消息不需要,因为当模态对话框或者的时候,本程序的消息指定都是发往模态对话框的

考虑非模态对话框的WinMain消息循环就应该:

1
2
3
4
5
6
7
8
while (GetMessage (&msg, NULL, 0, 0))
{
if (hDlgModeless == 0 || !IsDialogMessage (hDlgModeless, &msg))//如果没有非模态对话框句柄或者该消息不是非模态对话框的才往下走
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
}

IsDialogMessage不止会判断该消息是不是非模态对话框的,并且会把是非模态对话框的消息发送

hDlgModeless是一个对WinMain和对话框过程都可见的全局变量,为啥要对WndProc也可见呢?

得想办法该关闭非模态对话框的时候就得关上吧,这个关闭怎么才能让WinMain知道?

修改hDlgModeless值为NULL,

谁来修改?WndProc吗?WndProc只负责父窗口的逻辑,关于非模态对话框它不管

因此需要非模态对话框自己决定关闭hDlgModeless

COLORS3,但是摆大烂

windows甚至把专门调色的对话框都准备好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>
#include <commdlg.h>

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static CHOOSECOLOR cc ;
static COLORREF crCustColors[16] ;
cc.lStructSize = sizeof (CHOOSECOLOR) ;
cc.hwndOwner = NULL ;
cc.hInstance = NULL ;
cc.rgbResult = RGB (0x80, 0x80, 0x80) ;
cc.lpCustColors = crCustColors ;
cc.Flags = CC_RGBINIT | CC_FULLOPEN ;
cc.lCustData = 0 ;
cc.lpfnHook = NULL ;
cc.lpTemplateName = NULL ;

return ChooseColor(&cc) ;
}

就这么几行实现了一个调色的功能

image-20220824160618978

在老版本的windows绘图上,就是用的这个调色板,比如windows7上的

这不win7调色板吗,几天没见,这么拉了

实际上是调用ChooseColor(&cc)函数完成的,只需要给这个函数传递一个CHOOSECOLOR指针,就可以使用面板交互方式调制颜色了

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct tagCHOOSECOLORW {
DWORD lStructSize;//本结构体大小,冗余量
HWND hwndOwner;//调用ChooseColor函数产生的对话框的父窗口
HWND hInstance;//应用程序实例
COLORREF rgbResult;//RGB值,初始化一开始显式的颜色值
COLORREF *lpCustColors;//16色值,灰度,用于LowB电脑
DWORD Flags;//样式
LPARAM lCustData;//钩子过程参数
LPCCHOOKPROC lpfnHook;//钩子回调函数,需要Flags上置起CC_ENABLEHOOK标志
LPCWSTR lpTemplateName;//类名称或者ID
LPEDITMENU lpEditInfo;
} CHOOSECOLORW, *LPCHOOSECOLORW;

HEXCALC

不到150行源代码实现一个16进制计算器

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("HexCalc") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;

wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = DLGWINDOWEXTRA ; // Note!//30bytes
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (hInstance, szAppName) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;

if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}

hwnd = CreateDialog (hInstance, szAppName, 0, NULL) ;//创建非模态对话框

ShowWindow (hwnd, iCmdShow) ;//把对话框作为窗口

while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}

void ShowNumber (HWND hwnd, UINT iNumber)
{
TCHAR szBuffer[20] ;

wsprintf (szBuffer, TEXT ("%X"), iNumber) ;
SetDlgItemText (hwnd, VK_ESCAPE, szBuffer) ;//设置VK_ESCAPE控件的描述字
}

DWORD CalcIt (UINT iFirstNum, int iOperation, UINT iNum)
{
switch (iOperation)
{
case '=': return iNum ;
case '+': return iFirstNum + iNum ;
case '-': return iFirstNum - iNum ;
case '*': return iFirstNum * iNum ;
case '&': return iFirstNum & iNum ;
case '|': return iFirstNum | iNum ;
case '^': return iFirstNum ^ iNum ;
case '<': return iFirstNum << iNum ;
case '>': return iFirstNum >> iNum ;
case '/': return iNum ? iFirstNum / iNum: MAXDWORD ;
case '%': return iNum ? iFirstNum % iNum: MAXDWORD ;
default : return 0 ;
}
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static BOOL bNewNumber = TRUE ;
static int iOperation = '=' ;
static UINT iNumber, iFirstNum ;
HWND hButton ;

switch (message)
{
case WM_KEYDOWN: // left arrow --> backspace
if (wParam != VK_LEFT)
break ;
wParam = VK_BACK ;
// fall through
case WM_CHAR:
if ((wParam = (WPARAM) CharUpper ((TCHAR *) wParam)) == VK_RETURN)
wParam = '=' ;

if (hButton = GetDlgItem (hwnd, wParam))//字符消息的wParam就是字符的ASCII码,而对话框脚本中我们也把按钮的ID设置成了对应字符的ASCII码
{
SendMessage (hButton, BM_SETSTATE, 1, 0) ;//按下状态
Sleep (100) ;//按下状态持续0.1秒,让人能看见
SendMessage (hButton, BM_SETSTATE, 0, 0) ;//起来
}
else
{
MessageBeep (0) ;//没点到按钮就叫唤
break ;
}
// fall through
case WM_COMMAND:
SetFocus (hwnd) ;

if (LOWORD (wParam) == VK_BACK) // backspace
ShowNumber (hwnd, iNumber /= 16) ;//16进制数右移一位

else if (LOWORD (wParam) == VK_ESCAPE) // escape
ShowNumber (hwnd, iNumber = 0) ;

else if (isxdigit (LOWORD (wParam))) // hex digit//判断是否是16进制数,0-9,A-F
{
if (bNewNumber)//如果是新的计算局面
{
iFirstNum = iNumber ;//第一个操作数置为iNumber
iNumber = 0 ;//第二个操作数尚未到来,虚位以待
}
bNewNumber = FALSE ;//置局面混乱

if (iNumber <= MAXDWORD >> 4)
ShowNumber (hwnd, iNumber = 16 * iNumber + wParam -
(isdigit (wParam) ? '0': 'A' - 10)) ;
else
MessageBeep (0) ;
}
else // operation
{
if (!bNewNumber)//如果不是16进制数,即不是操作数,那就是操作符了
ShowNumber (hwnd, iNumber =CalcIt (iFirstNum, iOperation, iNumber)) ;
bNewNumber = TRUE ;
iOperation = LOWORD (wParam) ;
}
return 0 ;

case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}

窗口额外

cbClsExtra :windows程序为每一个窗口设计类管理一个WNDCLASS结构。在应用程序注册一个窗口类的时候,可以让windows分配一定字节空间的内存,这部分内存成为类的附件内存,有属于这个窗口类的所有窗口共享,类附件内存信息用于存储窗口类的附加信息。windows系统将这部分内存初始化为0,因此我们经常设置此参数为0.

cbWndExtra :windows程序为每一个窗口管理一个内部数据结构,在注册窗口类的时候,系统可以为每一个窗口分配一定的字节数的附加内存空间,称为窗口附件内存。应用程序可使用这部分内存存储窗口特有的数据,windows系统把这部分内存初始化为0.

HEXCALC.DLG

这个文件不在rc文件中,而是独立成文件,然后被包含进入RC文件

为啥不能直接放到RC里呢?因为visual studio没有添加对话框资源的选项

image-20220824165419486

这个对话框脚本是手打的,然后在资源脚本RC中导入:

1
2
3
4
5
3 TEXTINCLUDE DISCARDABLE 
BEGIN
"#include ""hexcalc.dlg""\r\n"
"\0"
END
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
/*---------------------------
HEXCALC.DLG dialog script
---------------------------*/

HexCalc DIALOG -1, -1, 102, 122
STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX//层叠,标题,系统菜单,最小方框
CLASS "HexCalc"//类名HexCalc
CAPTION "Hex Calculator"
{
PUSHBUTTON "D", 68, 8, 24, 14, 14//按钮 "D",字符68位置(8,24)宽高14
PUSHBUTTON "A", 65, 8, 40, 14, 14
PUSHBUTTON "7", 55, 8, 56, 14, 14
PUSHBUTTON "4", 52, 8, 72, 14, 14
PUSHBUTTON "1", 49, 8, 88, 14, 14
PUSHBUTTON "0", 48, 8, 104, 14, 14
PUSHBUTTON "0", 27, 26, 4, 50, 14
PUSHBUTTON "E", 69, 26, 24, 14, 14
PUSHBUTTON "B", 66, 26, 40, 14, 14
PUSHBUTTON "8", 56, 26, 56, 14, 14
PUSHBUTTON "5", 53, 26, 72, 14, 14
PUSHBUTTON "2", 50, 26, 88, 14, 14
PUSHBUTTON "Back", 8, 26, 104, 32, 14
PUSHBUTTON "C", 67, 44, 40, 14, 14
PUSHBUTTON "F", 70, 44, 24, 14, 14
PUSHBUTTON "9", 57, 44, 56, 14, 14
PUSHBUTTON "6", 54, 44, 72, 14, 14
PUSHBUTTON "3", 51, 44, 88, 14, 14
PUSHBUTTON "+", 43, 62, 24, 14, 14
PUSHBUTTON "-", 45, 62, 40, 14, 14
PUSHBUTTON "*", 42, 62, 56, 14, 14
PUSHBUTTON "/", 47, 62, 72, 14, 14
PUSHBUTTON "%", 37, 62, 88, 14, 14
PUSHBUTTON "Equals", 61, 62, 104, 32, 14
PUSHBUTTON "&&", 38, 80, 24, 14, 14
PUSHBUTTON "|", 124, 80, 40, 14, 14
PUSHBUTTON "^", 94, 80, 56, 14, 14
PUSHBUTTON "<", 60, 80, 72, 14, 14
PUSHBUTTON ">", 62, 80, 88, 14, 14
}

全是按钮的对话框,但是没有定义动作,我们需要给每个按钮按下之后的效果编程

这个怎么实现的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case WM_CHAR:
if ((wParam = (WPARAM) CharUpper ((TCHAR *) wParam)) == VK_RETURN)
wParam = '=' ;

if (hButton = GetDlgItem (hwnd, wParam))//字符消息的wParam就是字符的ASCII码,而对话框脚本中我们也把按钮的ID设置成了对应字符的ASCII码
{
SendMessage (hButton, BM_SETSTATE, 1, 0) ;//按下状态
Sleep (100) ;//按下状态持续0.1秒,让人能看见,知道自己按下去了
SendMessage (hButton, BM_SETSTATE, 0, 0) ;//起来
}
else
{
MessageBeep (0) ;//没点到按钮就叫唤
break ;
}

对话框脚本中故意设置按钮id和ASCII字符编码相同,因此非常方便

对话框模板

在COLORS3中,我们已经看到摆烂的力量,像调色板这种对话框模板再来一万个也不多

比如查找替换对话框

这些预定义好的对话框模板,都是以一个函数传递一个结构体指针创建的,这些结构体在commdlg.h中

用啥查啥的文档吧

windows SDK chapter 10 菜单与资源

资源

图标光标等资源不是重点,重点在菜单

图标,光标,菜单,对话框都是资源类型.保存在.exe或者.dll等文件中.程序使用函数显式或者隐式地将资源加载进入内存使用,比如LoadIcon和LoadCursor

注意到资源是保存在exe或者dll文件中的,这意味着,不需要另外保存bmp或者ico等文件格式了,要啥直接放到到exe文件中,万事不求人了

资源脚本

资源脚本以.RC为后缀

image-20220821171827617

在visual studio解决方案视图下,它是这样的

image-20220821171915530

如果用其他文本编辑器比如vscode打开ICONDEMO.rc,他实际上时类似于头文件的代码

它不存放任何资源文件,但是指出这些文件的路径还有ID编号

其中菜单的结构最明显

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
//Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// 从 TEXTINCLUDE 资源生成。
//
#ifndef APSTUDIO_INVOKED
#include "targetver.h"
#endif
#define APSTUDIO_HIDDEN_SYMBOLS
#include "windows.h"
#undef APSTUDIO_HIDDEN_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE 4, 2

/////////////////////////////////////////////////////////////////////////////
//
// 图标
//

// ID 值最低的图标放在最前面,以确保应用程序图标
// 在所有系统中保持一致。

IDI_ICONDEMO ICON "ICONDEMO.ico"
IDI_SMALL ICON "small.ico"

/////////////////////////////////////////////////////////////////////////////
//
// 菜单
//

IDC_ICONDEMO MENU
BEGIN
POPUP "文件(&F)"
BEGIN
MENUITEM "退出(&X)", IDM_EXIT
END
POPUP "帮助(&H)"
BEGIN
MENUITEM "关于(&A) ...", IDM_ABOUT
END
END

...

资源脚本.rc文件引用了Resource.h头文件,which是给资源编号的,每个资源有唯一的编号方便过程函数按图索骥

Resource.h允许c源程序和.rc资源描述文件引用相同的符号

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
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 使用者 ICONDEMO.rc

#define IDS_APP_TITLE 103

#define IDR_MAINFRAME 128
#define IDD_ICONDEMO_DIALOG 102
#define IDD_ABOUTBOX 103
#define IDM_ABOUT 104
#define IDM_EXIT 105
#define IDI_ICONDEMO 107
#define IDI_SMALL 108
#define IDC_ICONDEMO 109
#define IDC_MYICON 2
#ifndef IDC_STATIC
#define IDC_STATIC -1
#endif
// 新对象的下一组默认值
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS

#define _APS_NO_MFC 130
#define _APS_NEXT_RESOURCE_VALUE 129
#define _APS_NEXT_COMMAND_VALUE 32771
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 110
#endif
#endif

书上给出的忠告是,不要直接修改资源描述文件和资源头文件,让visual studio维护这些东西

visual studio使用RC.EXE工具编译资源在编译阶段把ICONDEMO.rc编译,然后在连接阶段随obj和lib进入exe文件

图标

怎么绘制图标无所谓,只要是.ico格式的都能当成图标,关键看程序怎么用这个图标

在程序中使用自定义图标,不再使用Load(NULL,IDI_APPLICATION)指定的系统默认图标

应该这样写:

1
wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ;

意思是往本应用程序实例中加载一个编号为IDI_ICON的图标

在visualstudio项目中可以添加资源

image-20220821173846421

添加Icon图标资源

image-20220821173911820

选择32*32,32位色,然后就可以在32*32的格格矩阵上画画了

image-20220821173956694

visual studio顶部会有工具栏

image-20220821174601043

画完了还得把以前的图标删了,让自己的图标顶置

image-20220822085630711

然后去资源视图改ID,在画画这里找一年也找不到

image-20220821175726126

给他改成ID=IDI_ICON1,visual studio会自动在rc文件和资源头文件中修改变化的,此时在资源头文件Resource.h中可以看到IDI_ICON1的宏定义了

1
#define IDI_ICON1                       129

此时在创建窗口类的时候再写

1
wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON1)) ;

就可以加载图标了

这个图标是放在项目根目录下的,如果删了他,对运行已经编译链接好的程序没有影响,但是再编译时会报错找不到文件

LoadIcon

1
2
3
4
HICON LoadIconA(
[in, optional] HINSTANCE hInstance,
[in] LPCSTR lpIconName
);

hInstance指定图标的文件来源,

NULL则为系统图标

本应用程序实例句柄则为本exe文件中包含的图标

lpIconName指定要加载的图标名.

可以使用字符串,也可以使用MAKEINTERESOURCE宏指定图标的宏定义

1
2
#define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
#define MAKEINTRESOURCEW(i) ((LPWSTR)((ULONG_PTR)((WORD)(i))))

i先强制转化为WORD类型,这意味着i的有效范围是16位,更高位直接扬了

资源就到此为止吧,再学就不礼貌了,该点菜了

菜单

一开始我还想直接看菜单不看前面的图标光标这种资源,看到资源脚本直接蒙蔽了

概念

主菜单,顶级菜单:标题栏下面,客户区顶的常驻菜单

子菜单,下拉菜单,弹出菜单:主菜单点击后下拉的菜单.这种菜单可以嵌套多层

状态:菜单有启用,禁用状态,启用时有选中和非选中状态.

菜单结构

每个菜单项有三个特征定义,分别是菜单显式什么,菜单ID或者句柄,属性

定义菜单

visual studio中可以向项目添加资源,其中就包括菜单

image-20220821163145869

编辑菜单的方式是交互式的,右侧的菜单编辑器可以调制菜单属性(幸好一个菜单的属性稀松)

image-20220821163233880

菜单属性

能够编辑的菜单属性有12个(实际11个,最下边那个提示是MFC编程用的)

image-20220821163420651

弹出菜单为TRUE则本菜单可以弹出子菜单

灰显指菜单栏变灰且不活动,即禁用了,点击不会产生WM_COMMAND消息

描述文字就是菜单键上的文字

如果描述文字前面加上一个&符号,则该菜单项第一个字符会带上下划线,方便Alt更改菜单时索引他就像这样 image-20220821165229684

已勾选为TRUE则该菜单左侧有一个对号,这是复选标记

image-20220821163918622

已启用和灰显作用类似,但是只是点了没反应,字体不会变灰

分隔就是把该菜单作为分割线,比如1123和3菜单中间这个杠

image-20220821164249794

关键是ID,这是窗口过程给该菜单设置逻辑的唯一凭证

这里的ID是宏定义,实际上的数值visualstudio给我们放到Resource.h中了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Microsoft Visual C++ 生成的包含文件。
// 供 chapter10.rc 使用
//
#define IDC_MYICON 2
#define IDD_CHAPTER10_DIALOG 102
#define IDS_APP_TITLE 103
#define IDD_ABOUTBOX 103
#define IDM_ABOUT 104
#define IDM_EXIT 105
#define IDI_CHAPTER10 107
#define IDI_SMALL 108
#define IDC_CHAPTER10 109
#define IDR_MAINFRAME 128
#define IDR_MENU1 129
#define ID_32771 32771
#define ID_32772 32772
#define ID_32773 32773
#define ID_32774 32774
#define ID_32775 32775
#define ID_32776 32776
#define ID_32777 32777
#define ID_32778 32778
#define IDC_STATIC -1

如果需要更多的资源则自己添加宏定义

程序中使用菜单

有多个可以指定菜单的地方,

最初可以指定菜单是在注册窗口类时

1
wndclass.lpszMenuName   = MAKEINTRESOURCEW(IDC_ICONDEMO);

然后在从创建窗口实例的时候可以指定菜单覆盖窗口类的菜单

1
2
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, hMenu, hInstance, nullptr);

这里hMenu句柄可以是LoadMenu打开的菜单资源

如果CreateWindow这里不指定菜单, 则默认使用窗口类的菜单.否则覆盖之

窗口实例创建之后还有改变菜单的方法

1
SetMenu(hwnd,hMenu)

菜单和消息

WM_MENUSELECT

当鼠标在菜单上移动时产生

LOWORD(wParam) 弹出菜单索引或者菜单ID
HIWORD(wParam) 选择标记
Value Meaning
MF_BITMAP0x00000004L Item displays a bitmap.
MF_CHECKED0x00000008L Item is checked.
MF_DISABLED0x00000002L Item is disabled.
MF_GRAYED0x00000001L Item is grayed.
MF_HILITE0x00000080L Item is highlighted.
MF_MOUSESELECT0x00008000L Item is selected with the mouse.
MF_OWNERDRAW0x00000100L Item is an owner-drawn item.
MF_POPUP0x00000010L Item opens a drop-down menu or submenu.
MF_SYSMENU0x00002000L Item is contained in the window menu. The lParam parameter contains a handle to the menu associated with the message.
lParam 包含所选项的菜单句柄

该消息用于暂时高亮菜单,显式完整文本描述

WM_COMMAND

最重要

表示点击了某个启用的菜单

LOWORD(wParam)菜单ID

下面的三个消息几乎用不到

WM_INITMENUPOPUP

要显示弹出菜单时的消息

wParam弹出菜单的句柄

LOWORD(lParam)弹出菜单的索引

HIWORD(lParam)1系统菜单,0其他菜单

WM_SYSCOMMAND

点击了系统菜单某个启用的菜单项

LWORD(wParam)菜单ID

LOWORD(lParam)鼠标x坐标,屏幕坐标

HIWORD(lParam)鼠标y坐标

WM_MENUCHAR

用户按下了Alt和不对应任何菜单项字符键.

或者弹出菜单时按下不对应任何菜单项的字符键

用于程序捕获并提醒快捷键不存在

LOWORD(wParam) 字符码

HIWORD(wParam) 选择吗

lParam 菜单句柄

该消息直接发送给DefWindowProc会导致操作系统MessageBeep

例程

菜单结构

visual studio中编辑菜单

使用文本编辑器打开MENUDEMO.rc文件,其菜单结构是按照BEGIN,END分块的

BEGIN,END块可以嵌套,约外层的菜单嵌套层数越低

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

IDC_MENUDEMO MENU
BEGIN
POPUP "文件(&F)"
BEGIN
MENUITEM "&New", IDM_FILE_NEW
MENUITEM "&Open", IDM_FILE_OPEN
MENUITEM "&Save", IDM_FILE_SAVE
MENUITEM "Save &As", IDM_FILE_SAVE_AS
MENUITEM "E&xit", IDM_APP_EXIT
MENUITEM SEPARATOR
END
POPUP "编辑(&E)"
BEGIN
MENUITEM "&Undo", IDM_EDIT_UNDO
MENUITEM SEPARATOR
MENUITEM "C&ut", IDM_EDIT_CUT
MENUITEM "&Copy", IDM_EDIT_COPY
MENUITEM "&Paste", IDM_EDIT_PASTE
MENUITEM "De&lete", IDM_EDIT_CLEAR
END
POPUP "背景(&B)"
BEGIN
MENUITEM "&White", IDM_BKGND_WHITE, CHECKED
MENUITEM "&Light Gray", IDM_BKGND_LTGRAY
MENUITEM "&Gray", IDM_BKGND_GRAY
MENUITEM "&Dark Gray", IDM_BKGND_DKGRAY
MENUITEM "&Black", IDM_BKGND_BLACK
END
POPUP "计时(&T)"
BEGIN
MENUITEM "&Start", IDM_TIMER_START
MENUITEM "S&top", IDM_TIMER_STOP, GRAYED
END
POPUP "帮助(&H)"
BEGIN
MENUITEM "&Help...", IDM_APP_HELP
MENUITEM "&About MenuDmo...", IDM_APP_ABOUT
END
END

菜单项前面加&,是为了加下划线,方便使用快捷键

整个菜单的ID表示在一开始列出了,是IDC_MENUDEMO,该值供注册窗口类或者实例化窗口时引用

1
2
IDC_MENUDEMO MENU
//标识符 资源类型

这个ID是多少不重要,只要保证每个资源有唯一的ID就可以了,我们只管给资源ID规定宏定义,visualstudio会自动帮我们分配一个正整数的

此后顶级菜单,即弹出菜单,都会有POPUP声明,然后跟着BEGIN,END作用块

比如POPUP "文件(&F)",这就声明了一个文件弹出菜单.其后面紧跟着的BEGIN,END包裹的块就是它的弹出菜单项

1
2
3
4
  POPUP "文件(&F)"//弹出菜单
BEGIN
...弹出菜单项
END

所有弹出菜单都不能有ID标识,只能是普通菜单项MENUITEM可以有ID标识,比如

1
MENUITEM "&New",                        IDM_FILE_NEW

因为弹出菜单只负责展示其菜单项,不能安排其他行为

程序中使用菜单

visual studio默认建立的资源都是使用ID索引的,没有使用字符串

刚才建立的菜单其ID为IDC_MENUDEMO,怎样才能让窗口使用这个菜单呢?

可以在创建窗口类的时候使用MAKEINTRESOURCE宏

1
wndclass.lpszMenuName = MAKEINTRESOURCE(IDC_MENUDEMO);

也可以在创建窗口实例的时候才指定菜单

1
2
3
4
5
6
7
8
wndclass.lpszMenuName = NULL;//创建窗口类的时候不指定菜单
...
HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDC_MENUDEMO));//加载菜单句柄
hwnd = CreateWindow(szAppName, TEXT("Menu Demonstration"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, hMenu, hInstance, NULL);

菜单消息

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

switch (message)
{
case WM_COMMAND:
hMenu = GetMenu(hwnd);

switch (LOWORD(wParam))
{
case IDM_FILE_NEW:
case IDM_FILE_OPEN:
case IDM_FILE_SAVE:
case IDM_FILE_SAVE_AS:

MessageBeep(0);//蜂鸣
return 0;
case IDM_APP_EXIT:
SendMessage(hwnd, WM_CLOSE, 0, 0);//发送程序退出关闭消息
return 0;

case IDM_EDIT_UNDO:
case IDM_EDIT_CUT:
case IDM_EDIT_COPY:
case IDM_EDIT_PASTE:
case IDM_EDIT_CLEAR:
MessageBeep(0);
return 0;

case IDM_BKGND_WHITE: // Note: Logic below //各种颜色背景是互斥关系,只能使用一个
case IDM_BKGND_LTGRAY: // assumes that IDM_WHITE
case IDM_BKGND_GRAY: // through IDM_BLACK are
case IDM_BKGND_DKGRAY: // consecutive numbers in
case IDM_BKGND_BLACK: // the order shown here.

CheckMenuItem(hMenu, iSelection, MF_UNCHECKED);//原来的选择弃选
iSelection = LOWORD(wParam);//对于WM_COMMAND消息来说,LOWORD(wParam)=点选菜单ID,这里获取新的点选背景ID
CheckMenuItem(hMenu, iSelection, MF_CHECKED);//修改新的背景色

SetClassLong(hwnd, GCL_HBRBACKGROUND, (LONG)GetStockObject(idColor[LOWORD(wParam) - IDM_BKGND_WHITE]));//修改系统属性,画刷颜色修改为新选中的颜色

InvalidateRect(hwnd, NULL, TRUE);
return 0;

case IDM_TIMER_START:
if (SetTimer(hwnd, ID_TIMER, 1000, NULL))//开始计时,时钟编号ID_TIMER(1)
{
EnableMenuItem(hMenu, IDM_TIMER_START, MF_GRAYED);//禁用
EnableMenuItem(hMenu, IDM_TIMER_STOP, MF_ENABLED);//启用计时结束实践
}
return 0;

case IDM_TIMER_STOP:
KillTimer(hwnd, ID_TIMER);//销毁时钟
EnableMenuItem(hMenu, IDM_TIMER_START, MF_ENABLED);
EnableMenuItem(hMenu, IDM_TIMER_STOP, MF_GRAYED);
return 0;
case IDM_APP_HELP:
MessageBox(hwnd, TEXT("Help not yet implemented!"),//摆烂
szAppName, MB_ICONEXCLAMATION | MB_OK);
return 0;

case IDM_APP_ABOUT:
MessageBox(hwnd, TEXT("Menu Demonstration Program\n")
TEXT("(c) Charles Petzold, 1998"),
szAppName, MB_ICONINFORMATION | MB_OK);
return 0;
}
break;

所有点击活动菜单的消息都有一个WM_COMMAND命令

此时其LOWORD(lParam)保存的是击中菜单的ID,如果击中的是弹出菜单则系统负载该逻辑,自动展开菜单,不需要我们指定弹出菜单的行为(事实上弹出菜单不允许拥有ID,这就意味着在程序中无法索引到它,无法安排行为)

复用关闭消息
1
2
case IDM_APP_EXIT:
SendMessage(hwnd, WM_CLOSE, 0, 0);//发送程序退出关闭消息

当点击菜单项项时,直接发送一体哦啊WM_CLOSE消息即可借助已有代码完成功能

改变勾选状态
1
2
3
4
5
DWORD CheckMenuItem(
HMENU hMenu,//菜单句柄,使用GetMenu(hwnd)获得
UINT uIDCheckItem,//需要修改状态的菜单项ID
UINT uCheck//修改后的状态
);
1
2
3
4
CheckMenuItem(hMenu, iSelection, MF_UNCHECKED);//原来的选择弃选
iSelection = LOWORD(wParam);//对于WM_COMMAND消息来说,LOWORD(wParam)=点选菜单ID,这里获取新的点选背景ID
CheckMenuItem(hMenu, iSelection, MF_CHECKED);//修改新的背景色
SetClassLong(hwnd, GCL_HBRBACKGROUND, (LONG)GetStockObject(idColor[LOWORD(wParam) - IDM_BKGND_WHITE]));//修改系统属性,画刷颜色修改为新选中的颜色

点选新的背景颜色之后,首先弃选先前的背景颜色,然后修改为新的背景颜色

之后修使用SetClassLong改程序的背景颜色

1
2
3
4
5
DWORD SetClassLongA(
[in] HWND hWnd,//需要修改属性的窗口句柄
[in] int nIndex,//需要修改的属性
[in] LONG dwNewLong//该属性的新值
);

菜单快捷键

右键菜单

在处理右键消息的时候可以安排上右键快捷菜单

1
2
3
4
5
6
7
8
9
BOOL TrackPopupMenu(
[in] HMENU hMenu,//需要显式的菜单句柄
[in] UINT uFlags,//点选菜单的方式
[in] int x,
[in] int y,
[in] int nReserved,
[in] HWND hWnd,
[in, optional] const RECT *prcRect
);

在指定的位置展示一个快捷菜单,并且追踪在该菜单上的选择.

该快捷菜单可以出现在任何地方

1
Displays a shortcut menu at the specified location and tracks the selection of items on the menu. The shortcut menu can appear anywhere on the screen.
1
2
3
4
5
6
7
8
9
10
case WM_CREATE:
hMenu = LoadMenu(hInst, MAKEINTRESOURCE(IDC_MENUDEMO));//hMenu获取整个菜单句柄
hMenu = GetSubMenu(hMenu, 0);//此时hMenu句柄是第一个顶级菜单的句柄,不是整个菜单的句柄,即文件弹出菜单,如果这里写1则为编辑弹出菜单
return 0;
case WM_RBUTTONUP:
point.x = LOWORD(lParam);
point.y = HIWORD(lParam);
ClientToScreen(hwnd, &point);
TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL);//允许使用左键和右键,在本菜单上作用相同
return 0;

键盘加速键

键盘加速键不是处理WM_KEYDOWN或者WM_CHAR复制键盘功能,当然这样写也能达到相同的目标

但是键盘加速键可以省去其逻辑

此处略去一大堆优点,因为有些优点现在我也体会不到

键盘加速键应当是Shift,Ctrl,Alt带领的跨界见,避免使用Tab,Enter,Esc,Space作键盘加速键

比如Ctrl+Z 撤销,Ctrl+X剪切,Del删除

加速键表

加速键表也是资源的一种,可以在visual studio中添加Accelerator资源

一方通行

加速键表长这样

ID就是该加速键的编号,键对应键盘动作,类型要么是虚拟键VIRTKEY,要么是字符CHAR

image-20220823114110566

如果键盘加速键想要和菜单关联起来,那么一个键盘加速键的ID需要设置成一个菜单项的ID

整个加速键表类似菜单一样,也有自己的一个ID标识,这个可以在资源视图看到

image-20220823145812581

通常方便使用,加速键表名和程序名,菜单名都相同

有了这个加速键表ID我们就可以在表的层面上和程序打交道,而不是需要负责每个加速键的逻辑.在程序中使用LoadAccelerators函数加载加速键表并获得句柄

1
HANDLE hAccel=LoadAccelerators(hInstance,MAKEINTRESOURCE(IDC_MENUDEMO));

现在程序中就有加速键表句柄了,下面就是如何使用该句柄了

在消息循环翻译分派消息之前先尝试翻译成加速键消息

1
2
3
4
5
6
while (GetMessage(&msg, NULL, 0, 0)) {
if (!TranslateAccelerator(hwnd, hAccel, &msg)) {//此处使用了加速键表句柄hAccel
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}

对于每个消息都会先经过一个加速键翻译,TranslateAccelerator函数确定保存在msg中的消息是否是键盘消息(包括虚拟键消息和字符消息),如果是,并且hAccel是一个有效的加速键表句柄,下面就调用hwnd指向窗口的窗口过程,向其发送同键盘加速键ID对应的菜单项按下的WM_COMMAND或者WM_SYSCOMMAND消息

显然这个翻译后的消息还会进入消息队列,但是翻译后的消息是WM_COMMAND消息,不是WM_KEYDOWN,WM_CHAR消息,不会再被TranslateAccelerator翻译,因此通过条件判断,正式进入消息循环处理

注意到TranslateAccelerator(hwnd, hAccel, &msg)这里指定了一个主窗口句柄,但是一个程序可以有多个窗口,这就会导致所有加速键消息发往主窗口.

如果想让一个消息发往现在的焦点窗口,则可以使用msg.hwnd,这个hwnd是目前消息的窗口句柄

处理WM_COMMAND消息

在处理WM_COMMAND消息时,加速键,菜单,控件消息的参数含义:

image-20220823151633079

如此看来,如果加速键ID和功能表ID故意设置成相同值,就不用大费周折了

例程

菜单表
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
IDC_MENUDEMO2 MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "&New", IDM_FILE_NEW
MENUITEM "&Open", IDM_FILE_OPEN
MENUITEM "&Save", IDM_FILE_SAVE
MENUITEM "Save &As", IDM_FILE_SAVE_AS
MENUITEM SEPARATOR
MENUITEM "&Print", IDM_FILE_PRINT
MENUITEM SEPARATOR
MENUITEM "E&xit", IDM_APP_EXIT
END
POPUP "&Edit"
BEGIN
MENUITEM "&Undo\tCtrl+Z", IDM_EDIT_UNDO
MENUITEM SEPARATOR
MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT
MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY
MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE
MENUITEM "De&lete\tDel", IDM_EDIT_DELETE
MENUITEM SEPARATOR
MENUITEM "&Select All", IDM_EDIT_SELECT_ALL
END
POPUP "&Help"
BEGIN
MENUITEM "&Help...\tF1", IDM_HELP_HELP
MENUITEM "&About...", IDM_APP_ABOUT
END
END
加速键表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
IDC_MENUDEMO2 ACCELERATORS
BEGIN
"8", IDM_UNDO, VIRTKEY, ALT, NOINVERT
VK_DELETE, IDM_EDIT_CLEAR, VIRTKEY, NOINVERT
VK_DELETE, IDM_EDIT_CUT, VIRTKEY, SHIFT, NOINVERT
VK_F1, IDM_HELP_HELP, VIRTKEY, NOINVERT
VK_INSERT, IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT
VK_INSERT, IDM_EDIT_PASTE, VIRTKEY, SHIFT, NOINVERT
"^C", IDM_EDIT_COPY, ASCII, NOINVERT
"^V", IDM_EDIT_PASTE, ASCII, NOINVERT
"^X", IDM_EDIT_CUT, ASCII, NOINVERT
"^Z", IDM_EDIT_UNDO, ASCII, NOINVERT
"^A", IDM_EDIT_SELECT_ALL, ASCII, NOINVERT
END

菜单名和加速键表ID都是IDC_MENUDEMO2,可以使用MAKEINTRESOURCE宏加载资源

使用父窗口控制编辑控件子窗口,大大减轻自己写编辑逻辑的工作量

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#include <windows.h> 
#include "MENUDEMO2.h"
#define ID_EDIT 1
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
TCHAR szAppName[] = TEXT("PopPad2");
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
HACCEL hAccel;
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(hInstance,szAppName);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = MAKEINTRESOURCE(IDC_MENUDEMO2);
wndclass.lpszClassName = szAppName;

if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR);
return 0;
}

hwnd = CreateWindow(//创建父窗口
szAppName, szAppName,
WS_OVERLAPPEDWINDOW,
GetSystemMetrics(SM_CXSCREEN) / 4,
GetSystemMetrics(SM_CYSCREEN) / 4,
GetSystemMetrics(SM_CXSCREEN) / 2,
GetSystemMetrics(SM_CYSCREEN) / 2,
NULL, NULL, hInstance, NULL
);

ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);

hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_MENUDEMO2));//加载加速键表资源
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(hwnd, hAccel, &msg))//翻译加速键
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
int AskConfirmation(HWND hwnd)//关闭父窗口时的询问
{
return MessageBox(hwnd, TEXT("Really want to close PopPad2?"),
szAppName, MB_YESNO | MB_ICONQUESTION);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hwndEdit;
int iSelect, iEnable;

switch (message)
{
case WM_CREATE:
hwndEdit = CreateWindow(TEXT("edit"), NULL,//创建子窗口,父窗口过程管理子窗口过程,子窗口是预定义好的编辑控件
WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |//子窗口|可见|横竖滚动|边框|左对齐|多行|自动滚动
WS_BORDER | ES_LEFT | ES_MULTILINE |
ES_AUTOHSCROLL | ES_AUTOVSCROLL,
0, 0, 0, 0, hwnd, (HMENU)ID_EDIT,//子窗口ID
((LPCREATESTRUCT)lParam)->hInstance, NULL);
return 0;

case WM_SETFOCUS:
SetFocus(hwndEdit);//父窗口将焦点让给子窗口
return 0;

case WM_SIZE:
MoveWindow(hwndEdit, 0, 0, LOWORD(lParam), HIWORD(lParam),TRUE);//父窗口移动子窗口,始终填充整个父窗口的客户区
return 0;

case WM_INITMENUPOPUP://初始化弹出菜单,每次点击顶级菜单时,在接收到WM_COMMAND之前先接收到WM_INITMENUPOPUP
if (lParam == 1)
{
EnableMenuItem(
(HMENU)wParam,IDM_EDIT_UNDO,
SendMessage(hwndEdit, EM_CANUNDO, 0, 0) ?
MF_ENABLED : MF_GRAYED
);

EnableMenuItem((HMENU)wParam,
IDM_EDIT_PASTE,
IsClipboardFormatAvailable(CF_TEXT) ?//询问剪切板状态,决定使能还是灰化粘贴键
MF_ENABLED : MF_GRAYED);

iSelect = SendMessage(hwndEdit, EM_GETSEL,0, 0);//获取高亮选中位置

if (HIWORD(iSelect) == LOWORD(iSelect))//LOWORD(iSelect)指向开始高亮的地方,HIWORD(iSelect)指向结束高亮的地方
iEnable = MF_GRAYED;//如果开始==结束说明没有选中东西
else
iEnable = MF_ENABLED;//如果开始!=结束说明选中东西了

EnableMenuItem((HMENU)wParam, IDM_EDIT_CUT, iEnable);//根据有没有选中东西决定复制,剪切,粘贴是否使能
EnableMenuItem((HMENU)wParam, IDM_EDIT_COPY, iEnable);
EnableMenuItem((HMENU)wParam, IDM_EDIT_CLEAR, iEnable);
return 0;
}
break;
case WM_COMMAND:
if (lParam)
{
if (LOWORD(lParam) == ID_EDIT &&
(HIWORD(wParam) == EN_ERRSPACE ||
HIWORD(wParam) == EN_MAXTEXT))
MessageBox(hwnd, TEXT("Edit control out of space."),
szAppName, MB_OK | MB_ICONSTOP);
return 0;
}
else switch (LOWORD(wParam))
{
case IDM_FILE_NEW:
case IDM_FILE_OPEN:
case IDM_FILE_SAVE:
case IDM_FILE_SAVE_AS:
case IDM_FILE_PRINT:
MessageBeep(0);//尚未实现
return 0;

case IDM_APP_EXIT:
SendMessage(hwnd, WM_CLOSE, 0, 0);
return 0;
case IDM_EDIT_UNDO:
SendMessage(hwndEdit, WM_UNDO, 0, 0);//编辑控件的预定义动作
return 0;

case IDM_EDIT_CUT:
SendMessage(hwndEdit, WM_CUT, 0, 0);//预定义动作,剪切,不需要手动实现
return 0;

case IDM_EDIT_COPY:
SendMessage(hwndEdit, WM_COPY, 0, 0);
return 0;
case IDM_EDIT_PASTE:
SendMessage(hwndEdit, WM_PASTE, 0, 0);
return 0;

case IDM_EDIT_CLEAR:
SendMessage(hwndEdit, WM_CLEAR, 0, 0);
return 0;
case IDM_EDIT_SELECT_ALL://Ctrl+A
SendMessage(hwndEdit, EM_SETSEL, 0, -1);
return 0;

case IDM_HELP_HELP:
MessageBox(hwnd, TEXT("Help not yet implemented!"),
szAppName, MB_OK | MB_ICONEXCLAMATION);
return 0;

case IDM_APP_ABOUT:
MessageBox(hwnd, TEXT("dustball's 卑鄙 notepad"),
szAppName, MB_OK | MB_ICONINFORMATION);
return 0;
}
break;

case WM_CLOSE:
if (IDYES == AskConfirmation(hwnd))
DestroyWindow(hwnd);
return 0;

case WM_QUERYENDSESSION:
if (IDYES == AskConfirmation(hwnd))
return 1;
else
return 0;

case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}

未实现的保存,打开,新建等功能需要等到学了对话框再说了

windows SDK chapter 21 动态链接库

库的类型

库类型 拓展名 作用 举例 使用时期 是否链接进入可执行目标文件
对象库(静态库) .lib .obj目标模块集合 LIBC.lib 开发时
导入库 .lib 链接器使用它解析函数调用 kernel32.lib 开发时
动态链接库 .dll 运行时提供支持 kernel32.dll 运行时

库的位置

类似于头文件的搜索顺序,当一个exe程序需要dll支持的时候,windows搜索dll也是有顺序的,编译链接时不需要指定动态库位置

1.exe文件同在的目录

2.shell的当前目录(pwd)

3.windows系统目录,即C://Windows/system32文件夹

4.windows目录,即C://Windows文件夹

5.DOS环境下PATH环境变量

如果在某一步找到则不再继续寻找

Visual C++和Visual Studio

由于这本书年代比较久远,当时的集成开发环境还是VC++,而现在win11上装一个兼容的VC++都不容易了

VC++中的"workspace"对应到VS上就是解决方案"solution"

一个工作区/解决方案 下面可以建立多个项目,一个项目对应一个应用程序或者一个DLL

Linux上的链接与库

此部分在CSAPP-chapter -7链接已经学过,在此只回顾一下GNU GCC命令

预编译

预编译,将源文件.c或者.cpp的宏定义展开成.i文件

1
cpp main.c > main.i

需要输出重定向到main.i,否则cpp命令默认输出到屏幕

汇编

汇编,将源文件汇编成汇编语言文件.s

1
gcc main.c -S

编译

编译,将源文件编译成可重定位目标文件.o

1
gcc main.c -c -o main.o

链接

将多个可重定位目标模块链接成可执行目标模块.out

1
gcc main.o func.o -o main.out

创建静态库

将一个或多个可重定位目标模块.o,创建成静态库.a(归档文件)

1
ar rcs libmain.a main.o

创建动态库

1
gcc -shared -fPIC main.c -o main.so

Visual Studio 创建使用库

Microsoft Visual Studio 2022

静态库的使用

创建静态库

1.新建空白解决方案

(在VC++上"解决方案"叫做workspace工作区)

image-20220721155939293
2.配置新"项目"

解决方案名称就叫做"MATHTOOLS"吧,

一个解决方案可以包含多个项目,但是新建解决方案时也是通过"新建项目"选项卡实现的

image-20220721160058473
3.新建静态库工程

在"解决方案资源管理器"中(视图->解决方案资源管理器或者Ctrl+Alt+L唤醒),

在"解决方案'MATHTOOLS'(0个项目)"上右键->新建项目

image-20220721160318596
4.在添加新项目搜索框中搜索"静态库"
image-20220721160358962
5.配置静态库项目

起个名,然后创建

image-20220721160433974
6.建立静态库项目之后的解决方案资源管理器
image-20220721160547138

有一些vs自动帮我们生成的文件,比如pch.h,framework.h,pch.cpp,MATHLIB.cpp

然而这些都是可有可无的

7.不使用预编译头

在解决方案资源管理器->MATHLIB项目 上右键,属性

image-20220721160748513

在弹出的属性页中将陪着和平台都改成所有的

image-20220721160818483

然后配置属性->C/C++->预编译头->不使用预编译头

image-20220721160906154
8.新建MathLib.h和MathLib.cpp

删掉头文件和源文件中vs帮我们建立好的文件,然后在头文件和源文件夹下面分别新建MathLib.h和MathLib.cpp

MathLib.h

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
#pragma once
struct Point {
double x;
double y;
Point(const double &x,const double &y);
Point();
void setCoordinate(const double& x, const double& y);
double& X();
double& Y();
const double getEuclidianDistance(const Point& p)const;
const double getManHatonDistance(const Point& p)const;
double getSlope(const Point& p)const;
friend std::ostream& operator<<(std::ostream& os, const Point& p);

};
struct Line {
Point a;
Point b;
Line();
Line(const Point& a, const Point& b);
double length()const;
Point &A();
Point &B();
double getSlope()const;
friend std::ostream& operator<<(std::ostream& os, const Line& l);
bool isParallel(const Line &l)const;
};

MathLib.cpp

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
#include <iostream>
#include <cmath>
#include <algorithm>
#include "MathLib.h"

Point::Point(const double& x, const double& y) {
this->x = x;
this->y = y;
}
Point::Point() {
this->x = 0;
this->y = 0;
}
void Point::setCoordinate(const double& x, const double& y) {
this->x = x;
this->y = y;
}
double& Point::X() {
return this->x;
}
double& Point::Y() {
return this->y;
}
const double Point::getEuclidianDistance(const Point& p)const {
return sqrt(1.0*(x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}
const double Point::getManHatonDistance(const Point& p) const{
return abs(x - p.x) + abs(y - p.y);
}
double Point::getSlope(const Point& p) const{
return 1.0*(y - p.y) / (x - p.x);
}
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << "," << p.y << ")";
return os;
}

Line::Line() {

}
Line::Line(const Point& a, const Point& b) {
this->a = a;
this->b = b;
}
double Line::length()const {
return a.getEuclidianDistance(b);
}
Point& Line::A() {
return a;
}
Point& Line::B() {
return b;
}
double Line::getSlope()const {
return a.getSlope(b);
}
std::ostream& operator<<(std::ostream& os, const Line& l ){
os << "[" << l.a << ";" << l.b << "]";
return os;
}
bool Line::isParallel(const Line& l)const {
return abs(getSlope() - l.getSlope()) < 0.001;
}



9.重新生成解决方案
image-20220721164200557

如果生成成功

image-20220721164221010

链接静态库

1.往解决方案中添加新项目
image-20220721164306283
image-20220721164320309
image-20220721164341661

新项目名叫MATHTEST

2.添加引用

此步的作用是

  1. 在链接A时,自动带上 b.lib,debug/release能够自动区分
  2. 当B项目有变化时,若编译A项目前先编译B项目
image-20220721170149106
image-20220721170201234
3.修改包含目录

此步的作用是给#include "MathLib.h"找到包含路径,即MathLib.h在哪里

image-20220721170408179
image-20220721170458649

到此MATHTEST中的源文件就能找到需要包含的MathLib.h头文件在哪里,链接器也能知道MathLib.lib在哪里了

4.测试静态库

在MATHTEST项目中新建一个源文件(或者利用自动创建的MATHTEST.cpp)

随便写点测试静态库的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "MathLib.h"
using namespace std;
int main()
{
Point A1(2, 4);
Point B1(4, 8);
Point A2(5, 10);
Point B2(15, 29);
Line L1(A1, B1);
Line L2(A2, B2);
cout << L1.isParallel(L2)<<endl;
cout << A1.getEuclidianDistance(B1) << " " << L1.length() << endl;
cout << A1 << endl;
cout << L1 << endl;
}

然后将MATHTEST项目设置为启动项目(如果让MATHLIB库项目作为启动项目根本起不来)

image-20220721171411117

然后Ctrl+F5开始运行(不调试)

image-20220721171456970

解决方案的结构

可以在解决方案根目录下面,终端上用tree /f命令打印整个结构

解决方案根目录

动态库的使用

创建动态库

1.建立空白解决方案
image-20220721182820966
image-20220721182929764
2.建立动态库项目
image-20220721182953735

具有导出项的动态库

image-20220721183016050

项目名称MATHDLL

image-20220721183052585

属性页设置不使用预编译头

image-20220721183151901

删除掉多余的pch等自动生成的文件之后,解决方案资源管理器是这样的

image-20220721183358281
3.充实头文件MATHDLL.h和原文件MATHDLL.cpp

自动生成的MATHDLL.h中有一个实用的宏定义MATHDLL_API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 MATHDLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// MATHDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef MATHDLL_EXPORTS
#define MATHDLL_API __declspec(dllexport)
#else
#define MATHDLL_API __declspec(dllimport)
#endif

// 此类是从 dll 导出的
class MATHDLL_API CMATHDLL {
public:
CMATHDLL(void);
// TODO: 在此处添加方法。
};

extern MATHDLL_API int nMATHDLL;

MATHDLL_API int fnMATHDLL(void);

MATHDLL_API这个宏定义,对于DLL库的头文件来说被翻译为__declspec(dllexport),

而对于包含该头文件的其他项目的源文件来说被翻译为__declspec(dllimport)

关于__declspec(dllexport)干了啥事,现在不想知道,只需要知道导出函数时必须

MATHDLL.h

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
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 MATHDLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// MATHDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef MATHDLL_EXPORTS
#define MATHDLL_API __declspec(dllexport)
#else
#define MATHDLL_API __declspec(dllimport)
#endif

struct MATHDLL_API Point {//使用MATHDLL_API修饰意思是导出类
double x;
double y;
Point(const double& x, const double& y);
Point();
void setCoordinate(const double& x, const double& y);
double& X();
double& Y();
const double getEuclidianDistance(const Point& p)const;
const double getManHatonDistance(const Point& p)const;
double getSlope(const Point& p)const;
friend std::ostream& operator<<(std::ostream& os, const Point& p);

};
struct MATHDLL_API Line {
Point a;
Point b;
Line();
Line(const Point& a, const Point& b);
double length()const;
Point& A();
Point& B();
double getSlope()const;
friend std::ostream& operator<<(std::ostream& os, const Line& l);
bool isParallel(const Line& l)const;
};


//extern MATHDLL_API int nMATHDLL;

//MATHDLL_API int fnMATHDLL(void);

MATHDLL.cpp

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
#include <iostream>
#include <cmath>
#include <algorithm>
#include "MATHDLL.h"

Point::Point(const double& x, const double& y) {
this->x = x;
this->y = y;
}
Point::Point() {
this->x = 0;
this->y = 0;
}
void Point::setCoordinate(const double& x, const double& y) {
this->x = x;
this->y = y;
}
double& Point::X() {
return this->x;
}
double& Point::Y() {
return this->y;
}
const double Point::getEuclidianDistance(const Point& p)const {
return sqrt(1.0 * (x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}
const double Point::getManHatonDistance(const Point& p) const {
return abs(x - p.x) + abs(y - p.y);
}
double Point::getSlope(const Point& p) const {
return 1.0 * (y - p.y) / (x - p.x);
}
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << "," << p.y << ")";
return os;
}

Line::Line() {

}
Line::Line(const Point& a, const Point& b) {
this->a = a;
this->b = b;
}
double Line::length()const {
return a.getEuclidianDistance(b);
}
Point& Line::A() {
return a;
}
Point& Line::B() {
return b;
}
double Line::getSlope()const {
return a.getSlope(b);
}
std::ostream& operator<<(std::ostream& os, const Line& l) {
os << "[" << l.a << ";" << l.b << "]";
return os;
}
bool Line::isParallel(const Line& l)const {
return abs(getSlope() - l.getSlope()) < 0.001;
}



dllMain.cpp保持原样不用管

此时重新生成解决方案没有错误,则动态库创建完毕了

此时在MATHTOOLS/debug/下面生成四个文件

1
2
3
4
5
6
7
8
9
10
11
12
PS C:\Users\86135\Desktop\testDLL\MATHTOOLS\Debug> ls


目录: C:\Users\86135\Desktop\testDLL\MATHTOOLS\Debug


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2022/7/21 19:01 52736 MATHDLL.dll
-a---- 2022/7/21 19:01 3532 MATHDLL.exp
-a---- 2022/7/21 19:01 6308 MATHDLL.lib
-a---- 2022/7/21 19:01 1200128 MATHDLL.pdb

有动态库dll这不足为奇,我们要的就是它

有pdb数据库也不足为奇,调试时用的符号就存在这个数据库里

那exp和lib是来干啥的?留作后话

到此动态库创建完毕,下面新建测试项目

链接动态库

链接一个动态库好麻烦,需要三个东西

1.包含头文件路径

2.lib导入库的路径

3.dll本尊的路径

其中头文件在编译的时候给一个符号,lib是一个药引子,它干了啥事留作后话

1.新建控制台测试项目
image-20220721190552713
image-20220721190612424
image-20220721190626516
2.添加引用

在MATHTEST的引用上添加引用,作用同静态库时的用法,即将lib文件链接进入exe文件

image-20220721190715859
image-20220721190737071

此步的作用是添加dll库文件本身的路径

3.添加头文件包含路径

头文件的作用是提供符号引用,只是提供引用的作用,没有其他作用了,不用想太多

有时甚至不包含头文件只链接对应源文件也不会链接报错,警告罢了

image-20220721190917081
image-20220721190857128
4.添加导入库(lib文件)路径

此处需要在链接器->输入->附加依赖项 还有 链接器->常规->附加库目录两个地方进行添加

image-20220721191127957
image-20220721192104087

到此程序就可以运行了

.obj->.lib->.dll

两种库的制作过程中,都有.lib文件,但是静态库中.lib是最终产品,动态库中.lib是一个药引子导入库

MSVC创建使用库

用Vscode打开一个文件夹作为工作目录,然后在本文件夹下面写EDRLIB.C和EDRLIB.H,EDRTEST.C

制作动态库

首先在本目录下面编写EDRLIB.H,EDRLIB.C两个源代码文件

EDRLIB.H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef __cplusplus //防止C++的命名重整,使得本库文件可以被C和C++程序使用
#define EXPORT extern "C" __declspec(dllexport)
#else
#define EXPORT __declspec(dllexport)
#endif

EXPORT BOOL CALLBACK EdrCenterTextA(HDC,PRECT,PCSTR);
EXPORT BOOL CALLBACK EdrCenterTextW(HDC,PRECT,PCWSTR);

#ifdef UNICODE
#define EdrCenterText EdrCenterTextW
#else
#define EdrCenterText EdrCenterTextA
#endif

EDRLIB.C

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
#include <windows.h>
#include "EDRLIB.H"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("StrProg");
HWND hwnd;
MSG msg;

WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;

if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("requires Windows NT"), szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(
szAppName,
TEXT("DLL Demonstrate Program"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);

ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
EdrCenterText(hdc, &rect, TEXT("in DLLLLLLLer"));
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hwnd, message, wParam, lParam);
}

使用MSVC编译链接制作动态库

1
2
cl /c EDRLIB.C
link /dll EDRLIB.obj user32.lib kernel32.lib gdi32.lib

到此本目录下面就生成了dll动态库文件和lib导入库文件

链接导入库

编写测试程序EDRTEST.C

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
#include <windows.h>
#include "EDRLIB.H"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("StrProg");
HWND hwnd;
MSG msg;

WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;

if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("requires Windows NT"), szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(
szAppName,
TEXT("DLL Demonstrate Program"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);

ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
EdrCenterText(hdc, &rect, TEXT("in DLLLLLLLer"));
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hwnd, message, wParam, lParam);
}

MSVC编译链接导入库

1
2
cl /c EDRTEST.c
link EDRTEST.obj EDRLIB.lib kernel32.lib user32.lib gdi32.lib

运行时使用动态库

由于EDRTEST.exe和EDRLIB.dll在同一目录下,因此装载器可以找到EDRLIB.dll,直接运行即可

image-20220819155710984

TDM-GCC创建使用库

我电脑上这个TDM-GCC来自于Dev-Cpp,把TDM-GCC的bin目录添加到环境变量path即可在终端上使用

文件都不变

制作动态库

1
gcc -shared EDRLIB.C -o EDRLIB.dll -luser32 -lgdi32 -lkernel32 

然而这一步完成之后只生成了dll动态库文件,没有生成lib导入库文件

链接动态库

1
gcc EDRTEST.C -o EDRTEST.exe -lkernel32 -lgdi32 -luser32 -L./ -lEDRLIB ;执行在当前目录下寻找库文件,库文件名EDRLIB

运行时使用动态库

dll和exe同目录,可以直接运行

1
./EDRTEST.exe
image-20220819161937488

效果和MSVC相同

如果把MSVC形成的exe和TDM-GCC形成的dll放在同一目录则运行报错,反过来也这样,总之就是谁的exe就得有谁的dll支持

DllMain

Dll文件也可以有入口点,DllMain函数,该函数会在Dll被装载或者卸载等等各时期被调用

1
int WINAPI DllMain(HINSTANCE hInstance,DWORD fdwReason,PVOID pvReserved);

参数意义

hInstance是本DLL模块的句柄,实际上是本模块加载进入进程地址空间中的基地址

fdwReason本DllMain函数被调用的原因

含义
DLL_PROCESS_ATTACH(1) 由于进程启动或调用 LoadLibrary,DLL 正在加载到当前进程的虚拟地址空间中。 DLL 可以使用此机会初始化任何实例数据,或使用 TlsAlloc 函数分配线程本地存储 (TLS) 索引。 lpvReserved 参数指示是静态还是动态加载 DLL。
DLL_PROCESS_DETACH(0) DLL 正在从调用进程的虚拟地址空间中卸载,因为它加载失败,或者引用计数已达到零, (进程每次调用 LoadLibrary) 时,都会终止或调用 FreeLibrarylpvReserved 参数指示是否由于 FreeLibrary 调用、加载失败或进程终止而卸载 DLL。 DLL 可以使用此机会调用 TlsFree 函数,以释放使用 TlsAlloc 分配的任何 TLS 索引,并释放任何线程本地数据。 请注意,接收 DLL_PROCESS_DETACH 通知的线程不一定是接收 DLL_PROCESS_ATTACH通知的 线程。
DLL_THREAD_ATTACH(2) 当前进程正在创建新线程。 发生这种情况时,系统会调用当前附加到进程的所有 DLL 的入口点函数。 调用是在新线程的上下文中进行的。 DLL 可以使用此机会初始化线程的 TLS 槽。 使用 DLL_PROCESS_ATTACH 调用 DLL 入口点函数的线程不会使用 DLL_THREAD_ATTACH 调用 DLL 入口点函数。 请注意,DLL 的入口点函数仅由进程加载 DLL 后创建的线程调用此值。 使用 LoadLibrary 加载 DLL 时,现有线程不会调用新加载 DLL 的入口点函数。
DLL_THREAD_DETACH(3) 线程已干净退出。 如果 DLL 已存储指向 TLS 槽中已分配内存的指针,则应使用此机会释放内存。 系统使用此值调用当前加载的所有 DLL 的入口点函数。 调用是在退出线程的上下文中进行的。

pvReserved尚未使用

分拣消息

DllMain的调用,和窗口过程函数WndProc的调用方式很像,

WndProc调用时,message参数记录消息类型.

DllMain调用时,fdwReason记录调用原因

因此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
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;

case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;

case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;

case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}

Windows消息钩取(Dll注入)

学习windows程序设计有一段时间了,我就知道了一个事,即消息会从操作系统的消息队列分发到应用程序的消息队列.比如键盘按下之后操作系统首先捕捉该消息,然后看看焦点窗口是谁,再把这个消息发给焦点窗口.

而消息钩取就发生在这个消息分发的过程中,钩子程序可以监视从操作系统消息队列发往应用程序消息队列的所有消息

windows消息流

当发生键盘按下的时间时,WM_KEYDOWN消息首先被添加到操作系统消息队列

操作系统判断是哪个应用程序发生的事件,然后从自己的消息队列中取出这个消息添加到对应应用程序的消息队列

应用程序的消息循环不停地从自己的消息队列中取出消息并调用相应的事件处理程序(即窗口过程)

这里窗口过程是回调形式的,因为同一个应用程序可以有多个窗口,自然可以有多个窗口过程,消息循环中的DispatchMessage(&msg);可以决定将消息发往哪个窗口过程

消息钩取发生在消息从OS消息队列到应用消息队列的路上

image-20220819173944182

SetWindowsHookEx数注册钩子

1
2
3
4
5
6
HHOOK SetWindowsHookEx(
int idHook, //hook type
HOOKPROC lpfn, //hook procedure
HINSTANCE hMod, //hook procedure所属的DLL句柄
DWORD dwThreadId //将要挂钩的目标线程ID
);

参数意义

idHook

钩子类型,决定钩子的类型和范围

img
lpfn

钩子过程函数,需要存在于某个DLL内部

该函数是一个回调函数,有固定的参数,返回值要求

1
2
3
4
5
6
LRESULT CALLBACK HookProc//程序员对于钩子过程的名字还是有支配权的
(
int nCode,//钩子代码,一种代码对应一种情况,钩子过程可以根据情况就事论事
WPARAM wParam,//两个附属参数都是nCode的更详细信息,nCode确定了wParam才有意义
LPARAM lParam,
);//返回值一般直接写返回CallNextHookEx的返回值就行了,根WndProc返回DefWndProc差不多

这里(nCode,wParam,lParam)的消息组合起始和WndProc中的(message,wParam,lParam)很像

nCode/message指定消息类型,wParam和lParam是具体的解释

hMod

钩子函数所在实例的句柄,即lpfn所在的DLL句柄

如果是本地钩子,并且钩子就写在本程序中,那么hMod可以是本程序实例句柄,此时lpfn注册的函数也在本程序中,编译之后一运行,程序和钩子都在同一个进程地址空间,钩子可以猥琐欲为

然而远程钩子,即这个进程钩取那个进程消息这种,就一定需要把钩子放在dll中,然后指望操作系统把这个dll,注入到远程进程的地址空间中了

dwThreadId

挂钩的线程ID

若该值为0则全局钩子,影响所有进程

返回值

HHOOK类型,即钩子句柄,钩子实际上也是一种内核数据结构,通过HHOOK句柄可以索引到该钩子

SetWindowsHookEx如果调用成功则返回其注册的钩子,方便后来使用完毕后删除钩子

UnhookWindowsHookEx删除钩子

1
2
3
BOOL UnhookWindowsHookEx(
[in] HHOOK hhk
);

只需要一个钩子句柄HHOOK,这个值是SetWindowsHookEx的返回值,即可注销这次SetWindowsHookEx注册的钩子

钩链

每调用一次SetWindowsHookEx就会注册一层钩子,调用多了就形成一条钩链,

最新注册的钩子最先截获消息

当前钩子回调函数处理完成之后调用CallNextHookEx将消息传递给钩链上下一个钩子回调函数进行处理

1
2
3
4
LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam){//钩子过程,回调函数
...
return CallNextHookEx(g_hHook,nCode,wParam,lParam);//轮到下一个钩子了,如果这里不再调用下一个钩子则一般情况下钩链到此为止(但是就有那特殊情况)
}

钩取notepad.exe键盘消息

当钩子和CPU架构不同的时候容易死机,死了n次得到的规律,64位的电脑上就老实编译64位程序,开64位notepad做实验吧

钩子模块KeyHook.dll

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
#include <stdio.h>
#include <windows.h>

#define DEF_PROCESS_NAME "notepad.exe"

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;//钩子句柄,由本模块自己维护
HWND g_hWnd = NULL;

BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH://如果是因为进程附加则g_hInstance获取hInstDLL值
g_hInstance = hInstDLL;
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}


LRESULT CALLBACK KeyBoardProc(int nCode,WPARAM wParam,LPARAM lParam){//钩子过程,回调函数
char szPath[MAX_PATH]={0,};
char *p=NULL;
if(nCode=0){
if(!(lParam&0x80000000)){
GetModuleFileNameA(NULL,szPath,MAX_PATH);
p=strrchr(szPath,'\\');
if(!_stricmp(p+1,DEF_PROCESS_NAME))return 1;
}
}
return CallNextHookEx(g_hHook,nCode,wParam,lParam);//轮到狗链上的下一个钩子了
}

#ifdef __cplusplus //防止C++的命名重整,使得本库文件可以被C和C++程序使用
#define EXPORT extern "C" __declspec(dllexport)
#else
#define EXPORT __declspec(dllexport)
#endif

EXPORT void HookStart(){//导出函数,启动钩子
g_hHook=SetWindowsHookEx(WH_KEYBOARD,KeyBoardProc,g_hInstance,0);
}

EXPORT void HookStop(){//导出函数,停止钩子
if(g_hHook){
UnhookWindowsHookEx(g_hHook);
g_hHook=NULL;
}
}
1
gcc KeyHook.cpp -shared -o KeyHook.dll

本模块导出了两个函数,注册和注销钩子

HookStart设置钩子类型为键盘型,并且注册了钩子过程回调函数KeyBoardProc,设置本模块为钩子所在模块,钩最后一个参数为0表明本钩子模块将被注入所有进程的地址空间

关键在于钩子过程回调函数KeyBoardProc,这个回调函数的返回值,参数个数,类型,意义,都是有固定要求的,除了函数名可以随便写

1
2
3
4
5
6
7
8
9
10
11
12
LRESULT CALLBACK KeyBoardProc(int nCode,WPARAM wParam,LPARAM lParam){//钩子过程,回调函数
char szPath[MAX_PATH]={0,};
char *p=NULL;
if(nCode==0){//当nCode=时对应键盘消息
if(!(lParam&0x80000000)){
GetModuleFileNameA(NULL,szPath,MAX_PATH);
p=strrchr(szPath,'\\');//返回szpath中最后一次出现'\'的位置
if(!_stricmp(p+1,DEF_PROCESS_NAME))return 1;
}
}
return CallNextHookEx(g_hHook,nCode,wParam,lParam);//轮到下一个钩链了
}

当GetMessage或者PeekMessage被调用时或者键盘消息发生时(WM_KEYUP,WM_KEYDOWN),操作系统会调用KeyBoardProc函数

这个函数叫啥无所谓,KeyBoardProc,HookProc等等,只要是SetWindowsEx把他注册上就行

对于键盘类型的钩子,其nCode有两种

HC_ACTION(0)和HC_NOREMOVE(3)

当nCode=HC_ACTION,此时wParam和lParam携带击键信息,和击键消息时的作用差不多

当nCode=HC_NOREMOVE,此时wParam和lParam携带击键信息,并且该消息尚未被从应用程序消息队列中移除

wParam是虚拟键编码

lParam

Bits Description
0-15 重复击键次数
16-23 OEM扫描码,基本用不到了
24 是否为拓展键
25-28 保留
29 如果Alt按下则为1
30 本键的先前状态,如果先前按下则为1
31 本键状态,如果正在按下则为0
if(nCode==0)

例子中首先判断nCode==0,这就确定了lParam和wParam代表的意义

if(!(lParam&0x80000000))

然后判断lParam的第31位是否是0,如果是0即对应键正在按下,则通过判断

GetModuleFileNameA(NULL,szPath,MAX_PATH);

本函数用于获取本进程已经加载的模块的完整路径名

1
2
3
4
5
DWORD GetModuleFileNameA(
[in, optional] HMODULE hModule,
[out] LPSTR lpFilename,
[in] DWORD nSize
);

hModule指定一个本进程已经加载的模块,如果是NULL则返回当前exe应用程序的完整路径

lpFilename用于承载本函数返回的路径的缓冲区

nSize指定lpFilename的大小,防止缓冲区溢出

本例子中这样写GetModuleFileNameA(NULL,szPath,MAX_PATH);

意思就是获取当前exe程序完整路径,放到szPath字符串数组中,该路径最长不能超过MAX_PATH长度

p=strrchr(szPath,'\\')

strrchr用于返回szPath字符串中最后一次出现\字符的指针位置

这里写了两个斜杠,第一个斜杠是转义的意思

if(!_stricmp(p+1,DEF_PROCESS_NAME))

stricmp相对于strcmp来说不区分字母大小写

这里DEF_PROCESS_NAME宏定义为"notepad.exe"

1
#define DEF_PROCESS_NAME "notepad.exe"

即这里判断了刚才返回的本exe文件的名称是否是"notepad.exe"

如果是则返回1,那么该键盘消息就在钩链上中止了,无法到达notepad应用程序的消息队列了,自然表现为无法获取键盘输入

这里钩子函数明明在KeyHook.dll中,那么怎么获取"本exe文件的名称"呢?

这是因为后来本dll文件会被操作系统强行注入exe进程的地址空间

前面进行了三次判断,要求

nCode==1表示键盘消息

lParam最高位为0表示键按下

当前应用程序名叫"notepad.exe"

如果有一次不满足则

1
return CallNextHookEx(g_hHook,nCode,wParam,lParam);

如果都满足则不再继续钩子,直接返回1

HookMain.exe

钩子以dll模块的形式打包,HookMain.exe相当于一个注射器,只要HookMain.exe以运行,钩子模块就会被注入所有进程的地址空间.

钩子自己判断了当前注入的进程,是不是notepad进程,如果是则中断键盘消息的传送,否则继续传递键盘消息

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
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();
PFN_HOOKSTART HookStart = NULL;
PFN_HOOKSTOP HookStop = NULL;
HMODULE hDll = NULL;
int main()
{
hDll = LoadLibraryA(DEF_DLL_NAME);

HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);

HookStart();//注册钩子
getchar();//如果不输入则钩子一直起作用
HookStop();
FreeLibrary(hDll);//注销钩子

return 0;
}
1
gcc HookMain.cpp -o HookMain.exe 

windows LIB文件格式

静态库

以Math.c为例,将其分别编译静态库文件libMath.lib

Math.c

1
2
3
4
5
6
7
8
9
__declspec(dllexport) double Add(double a,double b){
return a+b;
}
__declspec(dllexport) double Sub(double a,double b){
return a-b;
}
__declspec(dllexport) double Mul(double a,double b){
return a*b;
}

制作静态库

1
lib Math.c /NAME:libMath.lib

lib以一个文件魔数!<arch>\n开头,

接下来顺次是First Section,Second Section,Long Section,Obj Section.

如果本lib文件中包括了多个obj文件则ObjSection可以有多个.

Long Section用来存放太长的obj名称,如果没有超过16个字节的obj名则该节不存在

libMath.lib的结构

文件头魔数

不管是静态库还是导入库, 只要是lib文件,开头八个字节都得是文件魔数,

arch,archieve,归档

image-20220818144331204

节区

每个节区都是以一个结构体开始,可以认为是"节区头"

1
2
3
4
5
6
7
8
9
struct SectionHeader{
char Name[16]; // 节名称,即obj文件名
char Time[12]; // 时间
char UserID[6]; // 用户ID
char GroupID[6]; // 组ID
char Mode[8]; // 模式
char Size[10]; // 节区正文长度
char EndOfHeader[2];// 节头结束符
} ;

全都是字符串格式用字节存储,其好处是不管大小端机器,都可以兼容

很奇怪的是FirstSection采用大端模式,到了SecondSection就又成了小端存储了

节区头后面紧跟着是该节的节区正文

即每个节区都是节区头+节区正文格式

相邻两个节区之间可能存在胸垫填充,比如FirstSection的节区正文结束和SecondSection的节头开始之间就存在一个字节的填充

image-20220818164735293

怎么判断有没有填充呢?怎么寻找下一个节的开始位置呢?

每个节头大小是固定的60字节,最后两个字节一定是endMarker[2]="\60 \0A"

First Section

大端模式

包含库中所有符号名以及这些符号所在目标文件在本lib库文件中的偏移量

此处"所有"是指参与组成本lib文件的所有目标文件中的所有符号,不只是一个目标文件中的所有符号

其节名就用了一个字符"/"

Size指明了本节正文占用了多少个字节

节正文结构:

1
2
3
4
5
struct FirstSection{
unsigned long SymbolNum;//大端存储的符号数量
unsigned long SymbolOffset[SymbolNum];//符号所在目标节的偏移
char StrTable[m];//符号名称字符串表,m取决于所有符号的长度
}

对于libMath.lib的第FirstSection,其节区正文的16进制表示为:

1
2
3
4
5
6
7
8
9
10
11
SymbolNum = 00 00 00 03 

SymbolOffset[SymbolNum] =
00 00 00 C2
00 00 00 C2
00 00 00 C2

StrTable[m] =
5F 41 64 64 00
5F 4D 75 6C 00
5F 53 75 62 00

在32位机器上long和int一样长都是32位,因此unsigned long占用了前4个字节

由于大端存储因此03在最高位(最右侧)

SymbolNum=3,这与Math.c中正好有三个函数符号相吻合

SymbolOffset[3]={C2,C2,C2}这表明三个符号同属于一个目标模块,这个目标模块在本lib文件中的偏移量是C2

image-20220818152323244

C2位置确实是第一个也是唯一一个ObjSection的偏移量,这个ObjSection就是Math.obj

StrTable[m]=_Add._Mul._Sub.即所有符号名,每个符号名都以00结尾

Second Section

小端模式

内容和FirstSection相同,但是是一个有序表,查这个比查First Section来的快

名字也和FirstSection相同,都是"/"

正文结构:

1
2
3
4
5
6
7
struct SecondSection{
unsigned long ObjNum;//本库文件中的Obj节数量
unsigned long ObjOffset[ObjNum];//每个Obj节分别的偏移量,
unsigned long SymbolNum;//所有符号数量,作用和FirstSection.SymbolNum相同
unsigned short SymbolIdx[SymbolNum];//第i个符号所在的Obj节下标
char StrTable[m];//符号名表,同FirstSection.StrTable,第i个符号的符号名
}
1
2
3
4
5
6
7
8
9
10
11
ObjNum= 01 00 00 00 
ObjOffset[ObjNum] = C2 00 00 00
SymbolNum = 03 00 00 00
SymbolIdx[SymbolNum] =
01 00
01 00
01 00
StrTable[SymbolNum] =
5F 41 64 64 00 _Add\0
5F 4D 75 6C 00 _Mul\0
5F 53 75 62 00 _Sum\0

比较FirstSection和SecondSection

FirstSection SecondSection 意义是否相同
存储方式 大端 小端
记录符号个数 SymbolNum SymbolNum 相同
记录符号位置 SymbolOffset[i]第i个符号所在obj的节偏移量 SymbolIdx[i]第i个符号所在的obj节是第几个obj节 不同
记录符号名 StrTable[m] StrTable[m] 相同
记录Obj节数 ObjNum
记录每个Obj节的偏移量 ObjOffset[ObjNum]

Long Section

长名称节,存放太长的obj名

每个节开始时的SectionHeader会记录该节的信息,但是SectionHeader.Name只有16字节,如果一个obj文件名比如"LinkedDoubleList.obj"长度就超过了16字节,用Name[16]显然放不下,就得放到Long section节里,Name[16]存放的是"/<LongSection中的偏移量>"表明该节名需要去Long Section找,并且给出了相对于该节的位置

image-20220818154931447

Math.obj显然不够16个字节,本lib文件中没有该节

Obj Section

目标文件节,存放不同的目标文件的原始数据.

本节的节区正文相当于把COFF文件直接乎过来了

节头:

image-20220818154534122

从名称上可以看出Math.obj/

Size=794本节正文长794个字节

节区正文是直接抄的obj文件

节区正文
Math.obj

节区正文和Math.obj完全相同

导入库

还是以Math.c制作动态库时形成的导入库Math.lib为例

1
2
3
4
5
6
7
8
9
__declspec(dllexport) double Add(double a,double b){
return a+b;
}
__declspec(dllexport) double Sub(double a,double b){
return a-b;
}
__declspec(dllexport) double Mul(double a,double b){
return a*b;
}
1
2
cl Math.c /c
link /dll Math.obj

编译链接完成后同一目录下面形成Math.lib和Math.dll

此Math.lib就是导入库

image-20220818165520115

从节头名看,前两个节分别是FirstSection和SecondSection,后面就都是目标文件了

文件头魔数

所有lib文件不管是静态库还是导入库都一样!<arch>

节区

First Section

节头除了本节正文大小之外就没有什么有效信息了主要是看节区:

1
2
3
4
5
struct FirstSection{
unsigned long SymbolNum;//大端存储的符号数量
unsigned long SymbolOffset[SymbolNum];//符号所在目标节的偏移
char StrTable[m];//符号名称字符串表,m取决于所有符号的长度
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SymbolNum = 00 00 00 09 
SymbolOffset[SymbolNum] =
00 00 01 CA
00 00 03 E8
00 00 05 1C
00 00 06 68
00 00 06 68
00 00 07 24
00 00 07 24
00 00 06 C6
00 00 06 C6
StrTable[m] =
__IMPORT_DESCRIPTOR_Math: 5F 5F 49 4D 50 4F 52 54 5F 44 45 53 43 52 49 50 54 4F 52 5F 4D 61 74 68 00
__NULL_IMPORT_DESCRIPTOR: 5F 5F 4E 55 4C 4C 5F 49 4D 50 4F 52 54 5F 44 45 53 43 52 49 50 54 4F 52 00
Math_NULL_THUNK_DATA: 7F 4D 61 74 68 5F 4E 55 4C 4C 5F 54 48 55 4E 4B 5F 44 41 54 41 00
_Add: 5F 41 64 64 00
__imp__Add: 5F 5F 69 6D 70 5F 5F 41 64 64 00
_Sub: 5F 53 75 62 00
__imp__Sub: 5F 5F 69 6D 70 5F 5F 53 75 62 00
_Mul: 5F 4D 75 6C 00
__imp__Mul: 5F 5F 69 6D 70 5F 5F 4D 75 6C 00

可以看出本导入库中有9个符号,前三个符号

1
2
3
__IMPORT_DESCRIPTOR_Math
__NULL_IMPORT_DESCRIPTOR
Math_NULL_THUNK_DATA

是预定义的,就算我们啥也不写,照样有这三个符号

然后每个我们自己写的函数都有两个名字,比如

1
2
_Add
__imp__Add

这是x64符号名修饰造成的,实际上两个符号指向同一函数

Second Section

1
2
3
4
5
6
7
struct SecondSection{
unsigned long ObjNum;//本库文件中的Obj节数量
unsigned long ObjOffset[ObjNum];//每个Obj节分别的偏移量,
unsigned long SymbolNum;//所有符号数量,作用和FirstSection.SymbolNum相同
unsigned short SymbolIdx[SymbolNum];//第i个符号所在的Obj节下标
char StrTable[m];//符号名表,同FirstSection.StrTable,第i个符号的符号名
}
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
ObjNum = 06 00 00 00 
ObjOffset[ObjNum]=
CA 01 00 00
E8 03 00 00
1C 05 00 00
68 06 00 00
24 07 00 00
C6 06 00 00
SymbolNum=09 00 00 00
SymbolIdx[SymbolNum]=
04 00
06 00
05 00
01 00
02 00
04 00
06 00
05 00
03 00
StrTable[m]=
5F 41 64 64 00
5F 4D 75 6C 00
5F 53 75 62 00
5F 5F 49 4D 50 4F 52 54 5F 44 45 53 43 52 49 50 54 4F 52 5F 4D 61 74 68 00
5F 5F 4E 55 4C 4C 5F 49 4D 50 4F 52 54 5F 44 45 53 43 52 49 50 54 4F 52 00
5F 5F 69 6D 70 5F 5F 41 64 64 00
5F 5F 69 6D 70 5F 5F 4D 75 6C 00
5F 5F 69 6D 70 5F 5F 53 75 62 00
7F 4D 61 74 68 5F 4E 55 4C 4C 5F 54 48 55 4E 4B 5F 44 41 54 41 00

共有6个ObjSection段

每个ObjSection的节头中的名字都叫"Math.dll"

Obj Section

前三个Obj节都是关于三个预定义符号的,所有的导入库中他仨基本相同

后面三个Obj节是关于我们自定义的函数的,每个自定义函数自成一节,这也就解释了为啥默认状态下动态库的链接是以函数为单位,而静态库的链接是以模块为单位的了.导入库中每个函数自成一个模块

下面炎鸠后三节的结构,参考The Structure of import Library File (.lib) - CodeProject

以Add函数所在节为例

image-20220819104759118

节头表明正文部分有34字节

描述正文的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct SYMBOL_DESCRIPTOR          // size = 0x14
{
WORD a;
WORD b;
WORD c;
WORD Architecture;//程序可以运行的体系结构,比如x86,x86_64
DWORD Id;//随机生成的ID
DWORD Length;//符号名和库名 字符串总长度
union
{
WORD Hint;//最有可能的序号
WORD Ordinal;
WORD Value;
}
WORD Type;
};
1
2
3
4
5
6
7
8
9
10
11
00 00 
FF FF
00 00
4C 01 //Architecture,x86
F2 29 95 FA //随机数
0E 00 00 00 //_Add\0Math.dll\0 两个字符串的总长度(包括00)
00 00 //Hint=0,表示AddressOfNames中的下标
08 00 //Type=8,表示x86 __cdecl调用约定

5F 41 64 64 00 //_Add
4D 61 74 68 2E 64 6C 6C 00 //Math.dll

工具

MSVC

cl /c 只编译,不链接,生成目标模块

lib /EXTRACT:<目标文件> /OUT:<目标文件> 从lib文件中提取obj文件

lib Math.c /NAME:libMath.lib 制作静态库

多练练

菜就DLL

DLL刚用起来的时候感觉狠抽象

用MSVC编译生成DLL的同时还会伴随形成两个文件

dll,exp,lib他仨就跟那黄金三镖客似的

使用link命令链接的时候却不用指定dll文件,只需要指定lib文件.

但是运行的时候dll还得和exe在同一个目录下面,否则就报告缺少哪个哪个dll

要是用gcc链接吧,就可以直接链接dll,什么exp什么lib根本用不到

到底怎么才算是一个标准的写法?DLL怎么写,怎么编译,怎么链接.都是问题

DLL

windows上的动态库,特点不用废话了

创建DLL

__declspec(dllexport)

表示该符号(函数或者全局变量)是要从本DLL中导出的.

就DLL屁事多,创建so动态库的时候也没见声明为导出函数

可以不使用__declspec(dllexport),改为使用链接脚本导出符号

使用链接脚本

除了用__declspec声明导入导出函数,还可以使用.def文件控制链接过程

Math.c

1
2
3
4
5
6
7
8
9
double Add(double a,double b){
return a+b;
}
double Sub(double a,double b){
return a-b;
}
double Mul(double a,double b){
return a*b;
}

Math.def

1
2
3
4
5
LIBRARY Math
EXPORTS
Add
Sub
Mul

使用链接脚本编译Math.c

1
cl Math.c /LD /DEF Math.def

此步执行后还是生成了老四样

之后链接就按照"使用DLL"进行

Math.c

一个很简单的DLL文件Math.c,这里使用__declspec(dllexport)声明导出符号

1
2
3
4
5
6
7
8
9
10
__declspec(dllexport) double Add(double a,double b){
return a+b;
}
__declspec(dllexport) double Sub(double a,double b){
return a-b;
}
__declspec(dllexport) double Mul(double a,double b){
return a*b;
}

这里所有的函数都是用__declspec(dllexport)修饰为导出函数,作用是给链接器提供信息,如果不声明为导出函数,编译链接运行也都不会出错,但是运行的时候啥也不会发生

编译Math.c

使用MSVC工具编译生成dll文件

1
cl /LDd Math.c

此后生成了四个文件,obj,lib,exp,dll

1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\Users\86135\desktop\testDLL> ls


目录: C:\Users\86135\desktop\testDLL


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2022/8/8 15:47 222 Math.c
-a---- 2022/8/8 15:48 339968 Math.dll
-a---- 2022/8/8 15:48 810 Math.exp
-a---- 2022/8/8 15:48 1918 Math.lib
-a---- 2022/8/8 15:48 779 Math.obj
文件 作用
Math.obj 编译阶段完成的产物,目标文件
Math.lib 导入库,起到胶水作用
Math.exp
Math.dll 动态库本尊

使用DLL

__declspec(dllimport)

表示该符号是从其他DLL中导入的

使用DLL的过程就是引入DLL导出符号的过程,即导入过程

TestMath.c

同一目录下TestMath.c

1
2
3
4
5
6
7
#include <stdio.h>
__declspec(dllimport) double Sub(double,double);
int main(){
double result=Sub(1.0,8.9);
printf("result=%.2f",result);
return 0;
}

编译TestMath.c

首先将TestMath.c编译成目标文件TestMath.obj

1
cl /c TestMath.c

链接导入库

然后将TestMath.obj与刚才的库文件链接

1
link TestMath.obj Math.lib

生成了TestMath.exe

这就很奇怪了,为啥参与链接的是Math.lib一个静态库文件,而不是Math.dll动态库文件?

甚至直接链接dll文件会报错

1
2
3
4
5
PS C:\Users\86135\desktop\testDLL> link TestMath.obj Math.dll
Microsoft (R) Incremental Linker Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.

Math.dll : fatal error LNK1107: 文件无效或损坏: 无法在 0x300 处读取

直接链接exp文件也会出错

image-20220808160800411

运行TestMath.exe

1
2
./TestMath.exe
result=-7.90

运行时链接

前面使用DLL是装载时导入动态库.

现在要在运行时导入动态库

LoadLibray

装载一个dll到进程的地址空间,作用类似于linux上的dlopen

GetProcAddress

查找某dll库中的某个符号的地址,作用类似于linux上的dlsym

FreeLibrary

卸载一个已经加载过的dll库,作用类似于dlclose

运行时链接

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
#include <windows.h>
#include <stdio.h>
typedef double (*Func)(double,double);
Func getFunction(char *DllName,char *FuncName){

HINSTANCE hinstLib=LoadLibrary(DllName);
if(hinstLib==NULL){
printf("ERROR: loading failed\n");
return NULL;
}
Func function =(Func)GetProcAddress(hinstLib,FuncName);
if(!function){
printf("no such function in this module\n");
FreeLibrary(hinstLib);
return NULL;
}
return function;
}

int main(){
Func Add=getFunction("Math.dll","Add");
if(!Add){
printf("load function failed\n");
return 1;
}
double Sum=Add(5,7.9);
printf("Sum=%.2f\n",Sum);
return 0;
}

编译

1
cl Math.c

运行

1
2
./Math.exe
Sum=12.90

导出表

DataDirectory[0]

导出符号表是PE文件头的DataDirectory结构数组

1
2
3
4
5
6
7
8
PE头->
NT头->
NT可选头->
DataDirectory[0]->
IMAGE_EXPORT_DIRECTORY->
->AddressOfFunctions
->AddressOfNames
->AddressOfOrdinals
1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

这个数组的第一项就是导出表信息

1
2
3
4
DataDirectory[0]{
VirtualAddress导出表的相对虚拟地址
Size 导出表的大小
}

IMAGE_EXPORT_DIRECTORY

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;//本模块名字,指向"Math.dll"字符串
DWORD Base;//
DWORD NumberOfFunctions;//导出函数总数
DWORD NumberOfNames;//导出符号总数
DWORD AddressOfFunctions;//导出地址表EAT
DWORD AddressOfNames;//符号名表,保存导出函数的名字,按照字典序排列,方便按图索骥
DWORD AddressOfNameOrdinals;//名字序号对应表
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

导出地址表

导出地址表Export Address Table,EAT,存放的是导出函数在本库中的RVA

用IDA64观察这个位置

1
2
3
4
.rdata:000000018004F5B8 ; Export Address Table for Math.dll
.rdata:000000018004F5B8 ;
.rdata:000000018004F5B8 off_18004F5B8 dd rva Add, rva Mul, rva Sub
.rdata:000000018004F5B8 ; DATA XREF: .rdata:000000018004F5AC↑o

可以发现dd rva Add,rva Mul,rva Sub字样,意思就是Add,Mul,Sub三个函数相对虚拟地址,双击可以直接跳转到该函数的定义

转化成WORD数组得到:

1
2
3
unsigned short off_18004F5B8[6] = {
0x1000, 0x0000, 0x1040, 0x0000, 0x1020, 0x0000
};

由于imagebase=0x180000000,因此RVA=0x1000对应VA=0x180001000,正好就是Add函数的地址

1
.text:0000000180001000 Add             proc near    

然而如果不借助ida64,我们只能知道这里是一个函数的开始,但是不知道这里是哪个函数.

函数名表

函数名表中实际存放的就是字符串的指针数组,只不过这些字符串是函数的名字罢了

1
2
3
.rdata:000000018004F5C4 ; Export Names Table for Math.dll
.rdata:000000018004F5C4 ;
.rdata:000000018004F5C4 off_18004F5C4 dd rva aAdd, rva aMul, rva aSub

ida给出的解释是,0x18004F5C4这个位置是Math.dll的导出函数名表,其中有三个名字,分别是aAdd,aMul,aSub,

貌似导出地址表和函数名表建立了一一对应的关系.

导出地址表的第0项是Add函数的地址,符号名表的第0项恰好又是Add函数名字字符串的指针

导出地址表的第1项是aMul函数的地址,符号名表的第1项又恰好是Mul函数名字字符串的指针

这是因为所有的导出函数都有一个名字.如果有匿名的导出函数就堆不起来了.匿名函数必须要有一个导出地址但是不一定有导出符号,因此导出地址表的表项数是大于等于函数名表的表项数的.

那么如果有匿名函数时,再怎样建立其函数地址和函数名的关联呢?

通过第三张表,符号序号对应表

函数名符号对应表

1
2
3
.rdata:000000018004F5D0 ; Export Ordinals Table for Math.dll
.rdata:000000018004F5D0 ;
.rdata:000000018004F5D0 word_18004F5D0 dw 0, 1, 2 ; DATA XREF: .rdata:000000018004F5B4↑o

由于Math.dll中每个函数都具名,因此此时这个函数名符号对应表很奇怪,或者说很弱智

它的第i项上的值是i

1
2
3
Ordinals[0]=0
Ordinals[1]=1
Ordinals[2]=2

为啥要这样呢?这是一个历史遗留问题

历史遗留问题

早期的计算机内存很小,使用函数名就得存放字符串,这会占用大量内存.

因此当时函数导出不能使用函数名,而是使用函数序号.

这就相当于啥呢,监狱里给犯人编号,不管哪个狱警只要含1000号都是指同一个犯人.

函数序号是怎么编号的呢?一个函数在地址导出表中的下标假设是x,那么它的序号就是IMAGE_EXPORT_DIRECTORY.Base+x

如果一个exe程序需要导入这个函数,

它的导入表中只需要记录IMAGE_EXPORT_DIRECTORY.Base+x,

然后减去IMAGE_EXPORT_DIRECTORY.Base得到对应函数在Math.dll地址导出表EAT中的下标,

拿这个下标一查EAT表就得到了对应函数的相对虚拟地址了

然而为啥要加一个Base再减去他呢?这不多此一举吗?

书上并没有给出解释,我的想法是,假设A库有1000个导出函数,B库有500个刀殂函数,A库的Base=1,那么A库的函数序号会占用1~1000,如果B库和A库统一编号,则B库的Base就是1001.

现在的解决方案

在现在PC机都能达到4G,8G,16G内存的时代,显然没必要这么抠门.

但是以前的机器内存是以K或者M为单位的,以1976年的8086为例,它的内存有1M=1000K.

一个字符占用1B,假设一个库文件中有1K个函数,每个函数名字长10个字符,这就是10kB,已经占用了内存总量的1/100.

对于今天的x86_64一个16G内存的计算机,10K根本不算东西

于是现在使用符号导出,向后兼容保留了序号导入的方式

一个exe文件的导入表中保存的是符号名,如果要调用这个库函数,需要

先查对应库的函数名表,得到对应函数名字符串在函数名表中的下标i0,

用这个下标i0作为符号名序号对应表的下标去查这个Ordinals表,Ordinals[i0]-IMAGE_EXPORT_DIRECTORY.Base得到的又是一个下标i1

然后拿着这个新下标i1去查导出地址表EAT,EAT[i1]上面放着的就是对应函数的相对虚拟地址了

以书上给出的例子推导一遍

image-20220815165550845

现在需要调用Mul函数,

首先去AddressOfNames指向的函数名表查,由于这里的函数名是按照字典序排列的,可以使用二分查找加速.不管怎么,这里查找的结果是AddressOfNames[2]=Mul,也就是说,下标为2对应的是Mul

然后去AddressOfNames指向的符号序号对应表查AddressOfNames[2]=2,减去base(1)得到1,也就是说Mul在导出地址表EAT中的下标是1

然后去查AddressOfFunctions指向的导出地址表,AddressOfFunctions[1]=0x1020即Mul相对于其所在库基址的虚拟地址

向后兼容问题

为了兼容以前的序号导入,需要保证已有的函数序号不改变,

还得保证所有函数名在AddressOfNames这个函数名表中按照字典序排列

如何同时满足这两点要求呢?

现在时光回溯到七八十年代,假设Math.dll是当时开发的

假设原来的库有三个导出函数Add,Mul,Sub,一开始的时候没有符号名表(或者说有也不用),因此一开始没有导出符号,

老头子程序员(当时他还是个年轻人)给这三个函数手工编序号1,2,3(这个编号随意,只不过这样编最方便)考虑到base默认为1,那么分别对应导出地址表中的下标就得是0,1,2

于是AddressOfFunctions[0]=0x1000就是1号函数的地址,即Add函数在其库中的相对虚拟地址

函数的序号先后对函数的地址大小有要求吗?

没有,在编写math.c源代码时,哪一个函数先写哪一个的相对虚拟地址就小,

比如这里Mul在符号表中排第2比Sub的第3靠前,但是Mul的地址0x1020比Sub的地址0x1010大

image-20220815170420215

现在时光来到2202年

如今的程序员在符号名表中给这仨个函数加上了名字,此时已有的序号不能改,因此查符号表中Mul得到的下标一定得是1,同理符号表中查Add一定得到下标0,于是这三个函数的名字安排很简单

1
2
3
AddressOfNames[0]="Add";
AddressOfNames[1]="Mul";
AddressOfNames[2]="Sub";

到此完美解决了历史问题,即函数只有序号没有名字这个问题

下面考虑新增的函数怎么安排

假设要增加一个"Div"函数,这个字符串的字典序在"Add"和"Mul"之间.

因此安排上它之后AddressOfNames这个符号名表应该是

image-20220815171746124

原来查这个表中的Mul得到的下标是1,现在Div把它往后挤了一个位置,再查Mul得到的下标成2了,显然不能再拿着2去查原来的符号序号对照表了,因为原来的符号序号对照表的第2个是Sub的序号

这时候应该咋办呢?

新函数Div插入了AddressOfNames[1],那么原来的AddressOfNames[1]之后的各项顺延一个,AddressOfOrdinals数组中对应项也跟着顺延

1
2
3
4
5
6
for(int i=length(AddressOfNames);i>1;--i){
AddressOfNames[i]=AddressOfNames[i-1];
AddressOfOrdinals[i]=AddressOfOrdinals[i-1];
}
AddressOfNames[1]="Div";
AddressOfOrdinals[1]=4

这样查在AddressOfNames[i]中查Mul得到2,用2查AddressOfOrdinals[2]-base得到的还是原来的AddressOfFunctions中的下标.这就对应上了

还有一个问题没有解决, 新的函数怎么编号,新的函数放到地址导出表的那里?

地址导出表中的位置取决于编号,编号-base就是地址导出表中的下标,因此只需要确定编号

原来的编号已经有1,2,3了,那么4及之后的编号都可是使用,那么新函数Div可以获取任意一个大于等于4的编号.

假如给他的编号是4,那么4-base=4-1=3,那么导出地址表的AddressOfFunctions[3]就得是Div的相对虚拟地址

假如给他的编号是5,那么5-base=5-1=4,那么导出地址表的AddressOfFunctions[4]就得是Div的相对虚拟地址

...以此类推

如果给他的编号是5,那么4没有被使用,并且AddressOfFunctions[3]没有被使用,这合理吗?合理,由于编号4未使用,只要是日后再有新函数加进来,就可以给他分一个编号4,其相对虚拟地址就放在AddressOfFunctions[3].不流失不蒸发零浪费

书上这里就给了Div编号为5的情况

image-20220815172959162

指定导出符号

指定导出符号有多种方法,比如__declspec(dllexport)编译器拓展修饰符或者def链接脚本,或者link命令行上直接指定

__declspec(dllexport)

此种方法编译器会,在只编译不链接形成的,obj文件中的.drectve段中,加入链接指示(实际上就是命令行参数)

链接器处理obj文件时会把.drectve中的信息提取出来放到命令行上当作链接命令

1
PS C:\Users\86135\desktop\math> cl /c Math.c
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
SECTION HEADER #1
.drectve name
0 physical address
0 virtual address
53 size of raw data
B4 file pointer to raw data (000000B4 to 00000106)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
100A00 flags
Info
Remove
1 byte align

RAW DATA #1
00000000: 20 20 20 2F 44 45 46 41 55 4C 54 4C 49 42 3A 22 /DEFAULTLIB:"
00000010: 4C 49 42 43 4D 54 22 20 2F 44 45 46 41 55 4C 54 LIBCMT" /DEFAULT
00000020: 4C 49 42 3A 22 4F 4C 44 4E 41 4D 45 53 22 20 2F LIB:"OLDNAMES" /
00000030: 45 58 50 4F 52 54 3A 41 64 64 20 2F 45 58 50 4F EXPORT:Add /EXPO
00000040: 52 54 3A 53 75 62 20 2F 45 58 50 4F 52 54 3A 4D RT:Sub /EXPORT:M
00000050: 75 6C 20 ul

Linker Directives
-----------------
/DEFAULTLIB:LIBCMT
/DEFAULTLIB:OLDNAMES
/EXPORT:Add
/EXPORT:Sub
/EXPORT:Mul

因此这里使用编译器拓展修饰符的作用就相当于不写拓展修饰,但是使用link命令行

1
2
cl /c Math.c
link /dll Math.obj /EXPORT:Add /EXPORT:Sub /EXPORT:Mul /DEFAULTLIB:LIBCMT /DEFAULTLIB:OLDNAMES

后面这个/DEFAULTLIB:不写也可以,默认自带.LIBCMT全称Library C multithreaded,及VC的多线程C库

link命令行

1
2
cl /c Math.c
link /dll Math.obj /EXPORT:Add /EXPORT:Sub /EXPORT:Mul

/EXPORT:<函数名>指定这个函数为导出函数

.def链接脚本

Math.def

1
2
3
4
5
LIBRARY MATH
EXPORTS
Add
Sub
Mul
1
cl Math.c /LD /DEF Math.def

def链接脚本指定符号序号

1
2
3
4
5
6
LIBRARY Math
EXPORTS
Add@1
Sub@2
Mul@3
Div @4 NONAME

NONAME意思是匿名导出函数,只有序号没有名字

exp文件

创建Math.dll是总会跟着生成一个Math.lib和一个Math.exp.

其中Math.lib是导入库.

Math.exp只是一个中间过程产物,没有作用,即使删了也不影响Math.lib和TestMath.obj的链接.

那这个玩意儿是干啥用的呢?

链接器创建DLL文件时会进行两遍扫描.

第一遍扫描会遍历所有的目标文件,收集所有导出符号以创建DLL导出表,

这第一遍扫描只是为了建立一个导出表,为了方便就直接创建了一个exp文件存放这个导出表.这个exp文件也是一个标准PE/COFF文件

第二次扫描的时候该exp文件就和其他目标文件一样链接进入DLL.由于该exp文件中只有导出表,只读属性,因此exp文件的内容会合并到DLL的.rdata只读数据段

dll文件的导出表结构基本上是从exp照搬过来的

左exp右dll导出表

exp文件的整个.edata区就只有一个导出表还有它指向的三个数组的信息

exp.edata

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
.edata:0000000000000000 _edata          segment para public 'DATA' use64
.edata:0000000000000000 assume cs:_edata

//0x0到0x27是IMAGE_EXPORT_DIRECTORY结构体
.edata:0000000000000000 dd 0
.edata:0000000000000004 dd 0FFFFFFFFh
.edata:0000000000000008 dw 0
.edata:000000000000000A dw 0
.edata:000000000000000C dd offset szName ; "Math.dll" ;0x46
.edata:0000000000000010 dd 1
.edata:0000000000000014 dd 3
.edata:0000000000000018 dd 3
.edata:000000000000001C dd offset rgpv ;0x28
.edata:0000000000000020 dd offset rgszName ;0x34
.edata:0000000000000024 dd offset rgwOrd ;0x40


.edata:0000000000000028 rgpv dd offset Add ; DATA XREF: .edata:000000000000001C↑o
.edata:000000000000002C dd offset Mul
.edata:0000000000000030 dd offset Sub


.edata:0000000000000034 rgszName dd offset $N00001 ; DATA XREF: .edata:0000000000000020↑o
.edata:0000000000000034 ; "Add"
.edata:0000000000000038 dd offset $N00002 ; "Mul"
.edata:000000000000003C dd offset $N00003 ; "Sub"

.edata:0000000000000040 rgwOrd dw 0 ; DATA XREF: .edata:0000000000000024↑o
.edata:0000000000000042 dw 1
.edata:0000000000000044 dw 2

.edata:0000000000000046 ; TCHAR szName[40]
.edata:0000000000000046 szName db 'Math.dll',0 ; DATA XREF: .edata:000000000000000C↑o
.edata:000000000000004F $N00001 db 'Add',0 ;4fh ; DATA XREF: .edata:rgszName↑o
.edata:0000000000000053 $N00002 db 'Mul',0 ; ; DATA XREF: .edata:0000000000000038↑o
.edata:0000000000000057 $N00003 db 'Sub',0 ; DATA XREF: .edata:000000000000003C↑o
.edata:0000000000000057 _edata ends
[0,0x58)edata段

edata段的文件偏移为0,到0x57字节结束,一共0x58个字节,其中

[0,0x28) IMAGE_EXPORT_DIRECTORY

0到0x27共40个字节是IMAGE_EXPORT_DIRECTORY结构体,也就是DataDirectory[0].VirtualAddress指向的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
[0x28,0x34) 导出地址表

0x28到0x33这12个字节是IMAGE_EXPORT_DIRECTORY.AddressOfFunctions指向的地址导出表EAT

1
2
3
AddressOfFunctions[0]=0x60
AddressOfFunctions[1]=0x68
AddressOfFunctions[2]=0x70

这里0x60,0x68,0x70指向的是.edata段之后的UNDEF部分

1
2
3
4
5
UNDEF:0000000000000060 ; Segment type: Externs
UNDEF:0000000000000060 ; UNDEF
UNDEF:0000000000000060 extrn Add:near ; DATA XREF: .edata:rgpv↑o
UNDEF:0000000000000068 extrn Mul:near ; DATA XREF: .edata:000000000000002C↑o
UNDEF:0000000000000070 extrn Sub:near ; DATA XREF: .edata:0000000000000030↑o

每一项8个字节,是64位机器上一个指针的长度,看来应该是存放一个函数地址的地方.

显然这里是一个桩代码,因为exp文件不含这三个函数的定义,因此需要等到进入dll后才能确定三个函数的位置

[0x34,0x40) 函数名表

0x34到0x3F这12个字节是IMAGE_EXPORT_DIRECTORY.AddressOfNames指向的函数名表

1
2
3
IMAGE_EXPORT_DIRECTORY.AddressOfNames[0]=0x4F
IMAGE_EXPORT_DIRECTORY.AddressOfNames[0]=0x53
IMAGE_EXPORT_DIRECTORY.AddressOfNames[0]=0x57

而0x4F开始正好是三个函数名

1
2
3
.edata:000000000000004F $N00001         db 'Add',0              ; DATA XREF: .edata:rgszName↑o
.edata:0000000000000053 $N00002 db 'Mul',0 ; DATA XREF: .edata:0000000000000038↑o
.edata:0000000000000057 $N00003 db 'Sub',0 ; DATA XREF: .edata:000000000000003C↑o
[0x40,0x46) 符号序号对应表

0x40到0x45这6个字节是IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals指向的符号序号对照表

这个表的表项是字类型的序号整数值,因此三个表项占用三个字,六个字节

1
2
3
IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals[0]=0
IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals[1]=1
IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals[1]=1
[0x46,0x58) 字符串表

最后一段,用来存放字符串表,包括dll名,三个函数名

dll.rdata.导出表

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
.rdata:0000000180017DE0 ; Export directory for Math.dll
.rdata:0000000180017DE0 ;
.rdata:0000000180017DE0 dd 0 ; Characteristics
.rdata:0000000180017DE4 dd 0FFFFFFFFh ; TimeDateStamp
.rdata:0000000180017DE8 dw 0 ; MajorVersion
.rdata:0000000180017DEA dw 0 ; MinorVersion
.rdata:0000000180017DEC dd rva aMathDll ; Name
.rdata:0000000180017DF0 dd 1 ; Base
.rdata:0000000180017DF4 dd 3 ; NumberOfFunctions
.rdata:0000000180017DF8 dd 3 ; NumberOfNames
.rdata:0000000180017DFC dd rva off_180017E08 ; AddressOfFunctions
.rdata:0000000180017E00 dd rva off_180017E14 ; AddressOfNames
.rdata:0000000180017E04 dd rva word_180017E20 ; AddressOfNameOrdinals
.rdata:0000000180017E08 ;
.rdata:0000000180017E08 ; Export Address Table for Math.dll
.rdata:0000000180017E08 ;
.rdata:0000000180017E08 off_180017E08 dd rva Add, rva Mul, rva Sub
.rdata:0000000180017E08 ; DATA XREF: .rdata:0000000180017DFC↑o
.rdata:0000000180017E14 ;
.rdata:0000000180017E14 ; Export Names Table for Math.dll
.rdata:0000000180017E14 ;
.rdata:0000000180017E14 off_180017E14 dd rva aAdd, rva aMul, rva aSub
.rdata:0000000180017E14 ; DATA XREF: .rdata:0000000180017E00↑o
.rdata:0000000180017E14 ; "Add" ...
.rdata:0000000180017E20 ;
.rdata:0000000180017E20 ; Export Ordinals Table for Math.dll
.rdata:0000000180017E20 ;
.rdata:0000000180017E20 word_180017E20 dw 0, 1, 2 ; DATA XREF: .rdata:0000000180017E04↑o
.rdata:0000000180017E26 aMathDll db 'Math.dll',0 ; DATA XREF: .rdata:0000000180017DEC↑o
.rdata:0000000180017E2F aAdd db 'Add',0 ; DATA XREF: .rdata:off_180017E14↑o
.rdata:0000000180017E33 aMul db 'Mul',0 ; DATA XREF: .rdata:off_180017E14↑o
.rdata:0000000180017E37 aSub db 'Sub',0 ; DATA XREF: .rdata:off_180017E14↑o
[0x180017DE0,0x180017E38)

共58个字节,正好和exp.edata段对应,甚至每个字节意义都对应

成分 exp.edata dll.idata.导出表
IMAGE_EXPORT_DIRECTORY结构体 [0,0x28) [0x180017DE0,0x180017E08)
导出地址表 [0x28,0x34) [0x180017E08,0x180017E14)
函数名表 [0x34,0x40) [0x180017E14,0x180017E20)
符号序号对应表 [0x40,0x46) [0x180017E20,0x180017E26)
字符串表 [0x46,0x58) [0x180017E26,0x180017E38)

相当于把exp.edata段照搬到dll的0x180017DE0处,然后修改了到处地址表指向的函数地址.显然这个事应该是运行时动态链接器干的.

类似于GOT和PLT的机制

导入表

使用dumpbin观察Math.dll的导入表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS C:\Users\86135\desktop\Math> dumpbin /IMPORTS Math.dll
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file Math.dll

File Type: DLL

Section contains the following imports:

KERNEL32.dll
18000F000 Import Address Table
180017E68 Import Name Table
0 time date stamp
0 Index of first forwarder reference

470 QueryPerformanceCounter
233 GetCurrentProcessId
237 GetCurrentThreadId
...
DA CreateFileW
94 CloseHandle

Math.dll自己偷着导入了kernel32.dll等库中的函数,然而我们并没有显式调用这些函数,也没有在链接的时候指定要链接kernel32.lib导入库.

这是因为kernel32包含的C基本运行库是自动链接的

装载器会尽量少导入函数但是保证满足所有依赖.

DataDirectory[1]

1
2
3
4
5
6
7
8
PE头->
NT头->
NT可选头->
DataDirectory[1]->
IMAGE_IMPORT_DESCRIPTOR[]->
FirstThunk->IMAGE_IMPORT_BY_NAME


PE/COFF文件的NT可选头的DataDirectory[1]就是导入表的数据目录

DataDirectory[1].VirtualAddress指向的是一个IMAGE_IMPORT_DESCRIPTOR[]结构体数组,该结构体数组以一个全空的结构体元素结尾

DataDirectory[1].Size表明该结构体数组的总大小

因此可以得到这样的公式 \[ \frac{DataDirectory[1].Size}{sizeof(IMAGE\_IMPORT\_DESCRIPTOR)}-1=有意义的导入描述符个数 \]

IMAGE_IMPORT_DESCRIPTOR

每一个需要导入的都会对应一个IMAGE_IMPORT_DESCRIPTOR导入描述符

DataDirectory[1].VirtualAddress[0]就是kernel32.dll的导入描述符结构

1
2
3
4
5
6
7
8
9
10
11
   typedef struct _IMAGE_IMPORT_DESCRIPTOR {
__C89_NAMELESS union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//导入名称表INT,import name table
} DUMMYUNIONNAME;
DWORD TimeDateStamp;

DWORD ForwarderChain;
DWORD Name; //导入库名指针
DWORD FirstThunk;//导入地址表IAT,import address table
} IMAGE_IMPORT_DESCRIPTOR;

一开始时,OriginalFirstThunk和FirstThunk都是各自指向一个IMAGE_THUNK_DATA结构体数组

1
2
3
4
5
6
7
8
9
   typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;//序号
DWORD AddressOfData;//指向IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 *PIMAGE_THUNK_DATA32;

后来INT还是指向这个结构体数组不变,但是IAT需要填上函数的实际地址

IMAGE_IMPORT_BY_NAME

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;//该符号最有可能的序号
CHAR Name[1];//该符号的符号名
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

动态链接器拿到符号名之后还得先去查对应库导出的函数名表,用得到的下标再去查符号序号对应表,得到的值才是序号,减去base再查导出地址表得到真正的函数地址

如果Hint就是目标符号的序号,则直接减去base就可以查出导出地址了

因此动态链接器会首先尝试使用Hint作为目标符号的序号,减去base去查导出地址表.但是这种方法不一定命中,因为导入时认为的函数序号和库中的序号可能不同.因此需要验证是否命中.

书上没有给出验证方法,我的猜测是,用Hint查符号名称对应表得到的下标,拿去直接访问符号名表,看看对应符号名是否和IMAGE_IMPORT_BY_NAME.name相同,相同则命中

如果没有命中再用符号名从头查,符号名是最靠谱的

间接调用指令

TestMath主函数中调用了Math库的Sub函数

1
double result=Sub(1.0,8.9);

其反汇编指令

1
2
140001014:	ff 15 3e 52 01 00    	call   QWORD PTR [rip+0x1523e]        # 0x140016258
14000101a: f2 0f 11 44 24 20 movsd QWORD PTR [rsp+0x20],xmm0

这有一个间接调用

1
call   QWORD PTR [rip+0x1523e]

把内存中rip+0x1523e这个地址上的四字拿出来再当作一个64位内存地址,调用该地址

如果写成call rip+0x1523e就相当于直接调用rip+0x1523e这个地址

0x140016258这个地方是啥呢?IAT

1
2
3
.idata:0000000140016258 ; Imports from Math.dll
.idata:0000000140016258 ;
.idata:0000000140016258 extrn Sub:qword ; CODE XREF: main+14↑p

idata段,给Math.dll建立的的导入地址表IAT,其中只有一项,Sub的桩代码,占用四字64个字节

显然日后这个位置需要填入正确的函数地址,这样看IAT表就相当于GOT表

__declspec(dllimport)

TestMath.exe针对Math.dll的导入地址表IAT中只有一项,是因为在TestMath.c中只声明导入了一个函数

1
__declspec(dllimport) double Sub(double,double);

这个声明告诉编译器Sub符号是从外部导入的,关于它的调用指令啃腚是间接调用.

在引入__desclpec关键字之前,编译器是不知道一个函数是本地的还是导入的,它统一都生成直接调用指令.

如果是本模块的函数,则编译器给他写上正确的地址.如果本模块中没有

这个直接调用的操作数是一个桩地址,其上只有一条指令,跳转指令,跳转到真正的函数地址.这个真正的函数地址是链接器填上的

1
2
3
CALL 0x0040100C 
...
0x0040100C: JMP DWORD PTR [0x0040D11C]

0x4D11C还是在IAT表中,DWORD PTR [0x0040D11C]才是真正的函数地址

导入函数的定位过程

在这里插入图片描述

装载前,OriginalFirstThunk和FirstThunk指向不同的地方,但是其中的数据相同

装载结束后,导入函数定位完成,OriginalFirstThunk指向的INT表不变,但是FirstThunk指向的IAT变成了函数的实际地址

这里写图片描述

装载时重定位

Rebasing,重定基地址

DLL中的代码段不是地址无关的,都是以DLL的基地址为基准,计算位置.

一般情况下,EXE程序是第一个装载进入虚拟内存的,没人和他争抢虚拟地址空间,exe程序就可以准确的装载进入ImageBase指定的基地址.在32位windows上这个地址通常是0x400000

而DLL就没有这么幸运了,DLL的默认基地址是0x10000000,假设第一个DLL需要装载时其基地址没有被占用.当第二个DLL需要装载时,其ImageBase恰好和第一个DLL相同,但是这个坑已经有人占了,那么第二个DLL应该放到哪里呢?

满足地址16K对齐要求的前提下找一个能放开此DLL的地方塞进去.

但是问题又来了.

对于64位Math.dll,其基地址是0x180000000,对于需要rip相对寻址的符号比如函数尚且好说.

但是对于需要绝对寻址的符号比如全局变量,假设其原地址为0x180001000,现在由于其他库早于Math.dll装载进入了0x180000000这个位置,那么Math.dll就得另寻他处,比如0x180010000,那么这个全局变量就得放到0x180011000.

所有引用到它的指令都需要被重定位,将该全局变量的位置从0x180001000修改为0x180011000 \[ 新符号位置-旧符号位置=新ImageBase-旧ImageBase \] 问题又来了

如果一个DLL装载进入A进程地址空间的0x180000000,装载进入B进程地址空间的0x180011000,那么该DLL中的一个需要绝对寻址的符号,在A进程地址空间中所有关于它的指令的操作数都得是0x180001000,在B进程地址空间中是0x180011000.

那么两个进程就得各自持有一份DLL的拷贝,这与linux上位置无关的so动态库不同.so只需要在物理内存中存在一份,映射进入多个进程的地址空间.DLL得有几个进程就得在物理内存中有几个拷贝

系统DLL

系统DLL比如kernel32.dll,user32.dll,gdi32.dll等等

开发人员在设计操作系统的时候就已经给他们刻意安排了一个基地址,不大容易和其他dll冲突

windows XP 32位 系统dll imagebase
user32 77D10000h
kernel32 77E40000h
shell32 773A0000h
gdi32 77C40000h

导入函数绑定

系统DLL每次加载进入进程地址空间的位置基本是不变的,一个函数今天加载到0x1800001000,明天又加载到0x1800001000,后天,大后天,一辈子都是这个位置.那么每次程序运行时都进行符号解析重定位多是一件废事儿啊

可以在运行之间就"确定"库函数的地址,即地址绑定.如果真的在装载时发生重定位,那时候再重新计算符号的真正地址也不迟.

绑定前的导入表:

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
C:\Users\86135\Desktop\math>dumpbin TestMath.exe /IMPORTS
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file TestMath.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

MATH.dll
414110 Import Address Table
41A640 Import Name Table
0 time date stamp
0 Index of first forwarder reference

2 Sub

KERNEL32.dll
414000 Import Address Table
41A530 Import Name Table
0 time date stamp
0 Index of first forwarder reference

367 HeapFree
639 WriteConsoleW
46D QueryPerformanceCounter
...

地址绑定后的导入表:

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
C:\Users\86135\Desktop\math>dumpbin TestMath.exe /IMPORTS
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file TestMath.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

MATH.dll
414110 Import Address Table
41A640 Import Name Table
FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference

10001020 2 Sub

KERNEL32.dll
414000 Import Address Table
41A530 Import Name Table
FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference

6B815FE0 367 HeapFree
6B81F1F0 63A WriteConsoleW
6B819970 46D QueryPerformanceCounter
...

可以发现每个导入函数都有了一个绝对地址,然而此时还是在文件中,没有加载进入内存,就已经预料到导入符号的地址了

然而这个关闭ASLR之后用od调试运行TestMath.exe可以发现实际上kernel32.dll和Math.dll并没有装载进入这里的位置

工具

MSVC

调整MSVC环境变量

现在的x86_64机器上MSVC默认将源代码编译成64位程序,如果需要编译成32位程序,则需要调整环境

1
Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build

这个文件夹下有几个写好的修改环境的bat文件(微软不建议自己手敲代码修改环境,因为需要修改的变量比较多)

执行其中一共bat就可以修改环境

或者直接运行相应环境的shell工具

image-20220817095545711

cl /c 只编译不链接

cl /LD 创建动态库文件

1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\Users\86135\desktop\testDLL> cl /LDd Math.c
用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.31.31106.2
版权所有(C) Microsoft Corporation。保留所有权利。

Math.c
Microsoft (R) Incremental Linker Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.

/out:Math.dll
/dll
/implib:Math.lib
Math.obj
正在创建库 Math.lib 和对象 Math.exp
1
cl /LDd Math.c  #创建带有调试信息的Math.dll动态库

dumpbin

dumpbin /EXPORTS观察导出符号

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
PS C:\Users\86135\desktop\testDLL> dumpbin /EXPORTS Math.dll
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file Math.dll

File Type: DLL

Section contains the following exports for Math.dll

00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
3 number of functions
3 number of names

ordinal hint RVA name

1 0 00001000 Add
2 1 00001040 Mul
3 2 00001020 Sub

Summary

3000 .data
3000 .pdata
13000 .rdata
1000 .reloc
3C000 .text
1000 _RDATA

dumpbin /IMPORTS 观察导入符号

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
PS C:\Users\86135\desktop\Math> dumpbin /IMPORTS Math.dll
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file Math.dll

File Type: DLL

Section contains the following imports:

KERNEL32.dll
18000F000 Import Address Table
180017E68 Import Name Table
0 time date stamp
0 Index of first forwarder reference

470 QueryPerformanceCounter
233 GetCurrentProcessId
237 GetCurrentThreadId
...
DA CreateFileW
94 CloseHandle

Summary

2000 .data
1000 .pdata
A000 .rdata
1000 .reloc
E000 .text
1000 _RDATA

dumpbin /RELOCATIONS 观察重定位信息

editbin

editbin /REBASE:BASE=<新基址> 修改基地址

editbin /BIND 绑定导入函数

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
PS C:\Users\86135\desktop\Math> dumpbin /IMPORTS TestMath.exe
Microsoft (R) COFF/PE Dumper Version 14.31.31106.2
Copyright (C) Microsoft Corporation. All rights reserved.


Dump of file TestMath.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

Math.dll
140016258 Import Address Table
140020060 Import Name Table
0 time date stamp
0 Index of first forwarder reference

2 Sub

KERNEL32.dll
140016000 Import Address Table
14001FE08 Import Name Table
0 time date stamp
0 Index of first forwarder reference

370 HeapFree
64A WriteConsoleW
470 QueryPerformanceCounter
233 GetCurrentProcessId
237 GetCurrentThreadId
30A GetSystemTimeAsFileTime
38A InitializeSListHead
4F5 RtlCaptureContext
4FD RtlLookupFunctionEntry
504 RtlVirtualUnwind
3A0 IsDebuggerPresent
5E6 UnhandledExceptionFilter
5A4 SetUnhandledExceptionFilter
2F1 GetStartupInfoW
3A8 IsProcessorFeaturePresent
295 GetModuleHandleW
DA CreateFileW
503 RtlUnwindEx
27D GetLastError
564 SetLastError
149 EnterCriticalSection
3E0 LeaveCriticalSection
123 DeleteCriticalSection
386 InitializeCriticalSectionAndSpinCount
5D6 TlsAlloc
5D8 TlsGetValue
5D9 TlsSetValue
5D7 TlsFree
1C5 FreeLibrary
2CD GetProcAddress
3E6 LoadLibraryExW
145 EncodePointer
487 RaiseException
4FF RtlPcToFileHeader
2F3 GetStdHandle
64B WriteFile
291 GetModuleFileNameW
232 GetCurrentProcess
178 ExitProcess
5C4 TerminateProcess
294 GetModuleHandleExW
1F0 GetCommandLineA
1F1 GetCommandLineW
36C HeapAlloc
1B4 FlsAlloc
1B6 FlsGetValue
1B7 FlsSetValue
1B5 FlsFree
AA CompareStringW
3D4 LCMapStringW
26A GetFileType
18F FindClose
195 FindFirstFileExW
1A6 FindNextFileW
3AE IsValidCodePage
1CC GetACP
2B6 GetOEMCP
1DB GetCPInfo
412 MultiByteToWideChar
637 WideCharToMultiByte
253 GetEnvironmentStringsW
1C4 FreeEnvironmentStringsW
546 SetEnvironmentVariableW
57F SetStdHandle
2F8 GetStringTypeW
2D4 GetProcessHeap
1B9 FlushFileBuffers
21A GetConsoleOutputCP
216 GetConsoleMode
268 GetFileSizeEx
555 SetFilePointerEx
375 HeapSize
373 HeapReAlloc
94 CloseHandle

Summary

2000 .data
2000 .pdata
B000 .rdata
1000 .reloc
15000 .text
1000 _RDATA

windows SDK chapter 7 mouse

鼠标的基本信息

是否在线

1
fMouse=GetSystemMetrics(SM_MOUSEPRESENT);

如果鼠标都不在线(和鼠标共用接口的输入设备也有可能被作为鼠标),则该函数返回0.

如果至少有一个鼠标在线则函数返回非零值

单键双键

1
cButtons = GetSystemMetrics(SM_CMOUSEBUTTONS);

但是在我的笔记本电脑上,这个值是8,而我的鼠标就四个键

可能是触摸板加上了两指三指四指动作,等等各种使用方法?

鼠标样式

鼠标样式最常见的就是斜向箭头,当程序忙的时候可能变成沙漏或者左箭头右沙漏

当绘图的时候鼠标可能会变成十字

实际上这个图标就是一个小的位图结构,点击鼠标时有效的位置只有一个像素点,叫做"焦点"

斜向箭头的焦点在顶点上.十字的焦点在十字路口

设置鼠标样式

在实例化窗口对象的时候就指定过一次鼠标样式了

1
wndclass.hCursor  = LoadCursor(NULL, IDC_ARROW);

IDC_ARROW就是最常见的斜向箭头样式

Value Meaning
IDC_APPSTARTINGMAKEINTRESOURCE(32650) Standard arrow and small hourglass
IDC_ARROWMAKEINTRESOURCE(32512) Standard arrow
IDC_CROSSMAKEINTRESOURCE(32515) Crosshair
IDC_HANDMAKEINTRESOURCE(32649) Hand
IDC_HELPMAKEINTRESOURCE(32651) Arrow and question mark
IDC_IBEAMMAKEINTRESOURCE(32513) I-beam
IDC_ICONMAKEINTRESOURCE(32641) Obsolete for applications marked version 4.0 or later.
IDC_NOMAKEINTRESOURCE(32648) Slashed circle
IDC_SIZEMAKEINTRESOURCE(32640) Obsolete for applications marked version 4.0 or later. Use IDC_SIZEALL.
IDC_SIZEALLMAKEINTRESOURCE(32646) Four-pointed arrow pointing north, south, east, and west
IDC_SIZENESWMAKEINTRESOURCE(32643) Double-pointed arrow pointing northeast and southwest
IDC_SIZENSMAKEINTRESOURCE(32645) Double-pointed arrow pointing north and south
IDC_SIZENWSEMAKEINTRESOURCE(32642) Double-pointed arrow pointing northwest and southeast
IDC_SIZEWEMAKEINTRESOURCE(32644) Double-pointed arrow pointing west and east
IDC_UPARROWMAKEINTRESOURCE(32516) Vertical arrow
IDC_WAITMAKEINTRESOURCE(32514) Hourglass

然而在我的win11笔记本上,除了斜向箭头和转圈,其他鼠标样式都加载不出来,或者都加载成转圈或者斜向箭头

客户区鼠标消息

windows只把键盘消息发往具有输入焦点的窗口,但是鼠标不同

只要鼠标经过某个窗口,windows就会对齐发送一个WM_MOUSEMOVE的消息

image-20220813092214535

其中双击信息许哟啊在创建窗口实例的时候指明风格使用

1
wndclass.style=CS_DBLCLKS | ...

带有CS_DBLCLKS风格的窗口才可以接收WM_LBUTTONDBLCLK这种双击消息

对于鼠标消息(hwnd,message,wParam,lParam)

lParam

包含鼠标的位置信息,低字表示x坐标,高字表示y坐标

1
2
x=LOWORD(lParam);
y=HIWORD(lParam);

wParam

包含了鼠标哪个键,还有此时Ctrl和Shift的状态.

令wParam和宏定义按位与即可测试相应状态

1
2
3
4
5
6
7
8
9
#ifndef NOKEYSTATES
#define MK_LBUTTON 0x0001
#define MK_RBUTTON 0x0002
#define MK_SHIFT 0x0004
#define MK_CONTROL 0x0008
#define MK_MBUTTON 0x0010
#define MK_XBUTTON1 0x0020
#define MK_XBUTTON2 0x0040
#endif

MOUSEMOVE的速度

windows不会给鼠标经过的每个像素点都产生一个WM_MOUSEMOVE消息,这取决于应用程序处理WM_MOUSEMOVE的速度,当应用程序的消息队列中还有WM_MOUSEMOVE的消息时就不能接收第二个WM_MOUSEMOVE消息

书上在此给出了一个例子.

凡是WM_MOUSEMOVE捕获的点都会被计入点集,点集中任意两个点连一条线

分析一下其过程函数WndProc

变量定义

1
2
3
4
5
static POINT pt[MAXPOINTS];//存储已经捕获的点集
static int iCount;//记录已经捕获的点数量
HDC hdc;//设备环境句柄
int i, j;//循环变量
PAINTSTRUCT ps;//绘图结构

鼠标信息处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case WM_LBUTTONDOWN:
iCount = 0;//左键按下时清零重新记录
InvalidateRect(hwnd, NULL, TRUE);//清零后重绘
return 0;

case WM_MOUSEMOVE:
if (wParam & MK_LBUTTON && iCount < MAXPOINTS)//如果是左键按下的移动状态并且目前点集未满则捕获新点
{
pt[iCount].x = LOWORD(lParam);//捕获新点
pt[iCount++].y = HIWORD(lParam);

hdc = GetDC(hwnd);
SetPixel(hdc, LOWORD(lParam), HIWORD(lParam), 0);//将该新点的位置打印成一个黑点
ReleaseDC(hwnd, hdc);
}
return 0;

case WM_LBUTTONUP:
InvalidateRect(hwnd, NULL, FALSE);//左键松开,立刻通知处理WM_PAINT函数,重绘点集
return 0;

绘图消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);

SetCursor(LoadCursor(NULL, IDC_WAIT));//由于绘图可能时间较长,因此此时将光标样式换成等待
ShowCursor(TRUE);//显示光标

for (i = 0; i < iCount - 1; i++)//每两个点之间握手一次
for (j = i + 1; j < iCount; j++)
{
MoveToEx(hdc, pt[i].x, pt[i].y, NULL);//从pt[i]到pt[j]连线
LineTo(hdc, pt[j].x, pt[j].y);
}
ShowCursor(FALSE);
SetCursor(LoadCursor(NULL, IDC_ARROW));//绘图完毕,光标从忙状态换成指针状态
EndPaint(hwnd, &ps);
return 0;

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

效果

左鼠标移速快,右鼠标移速慢

双击

只有创建窗口实例时,风格上允许双击的窗口才可以接受双击信息

1
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

对于没有双击风格的窗口,双击动作造成的消息:

WM_LBUTTONDOWN 左键第一次按下

WM_LBUTTONUP 左键第一次起来

WM_LBUTTONDOWN 左键第二次按下

WM_LBUTTONUP 左键起来

对于有双击风格的窗口,双击造成的信息:

WM_LBUTTONDOWN

WM_LBUTTONUP

WM_LBUTTONDBLCLK 第二次按下被替换为WM_LBUTTONDBLCLK

WM_LBUTTONUP

非客户区鼠标消息

image-20220813105023942

非客户区的消息在WM_前缀之后又加了一个NC前缀(not client)

参数意义

lParam

低字为x坐标,高字为y坐标,此处的坐标是相对于整个屏幕的坐标

而客户区的鼠标信息中lParam携带的坐标是相对于客户区左上角的坐标

image-20220813105412718

屏幕坐标和客户区坐标的互换:

1
2
3
4
5
6
7
8
BOOL ScreenToClient(
[in] HWND hWnd,
LPPOINT lpPoint
);
BOOL ClientToScreen(
[in] HWND hWnd,
[in, out] LPPOINT lpPoint
);

白刀子进,红刀子出

使用pt带着原坐标进去,带着转换坐标出来

wParam

非客户区鼠标移动或者单击的位置(不是坐标),一个标识符

击中测试

WM_NCHITTEST

关于鼠标的最后一个消息类型

这个消息优先级高于所有客户区和非客户区的鼠标消息

lParam

鼠标位置的屏幕坐标

wParam

没有用到

这条消息应该被直接传递给DefWindowProc,操作系统负责将屏幕坐标翻译为客户区坐标之后,产生一个客户区鼠标消息发送给应用程序

那么如果捕获该消息并且不让他传递给DefWindowProc,就阻断了所有鼠标消息.所有本窗口的鼠标动作将失效

什么是击中测试

在文件浏览器中双击某个文件时,文件浏览器会进入该目录或者打开该文件.

可是文件浏览器是怎么知道应该打开哪个文件的呢?

他需要获取鼠标位置然后判断这个位置落在哪里

image-20220814083834575

考虑实现一个简单的文件浏览器,以列表形式列出当前目录下的所有文件和子文件夹

每个文件占一行,放不下就用滚动条

客户区的点击动作就需要将纵坐标换算为行数,再根据卷动情况判断是指向的哪一行

根据确定的行号作为下标查文件名表,查到之后打开该文件,如果是文件夹则打开该文件夹

击中测试例程

书上在此给出了一个击中测试的例程,分析其过程函数WndProc

变量定义

1
2
3
4
5
6
7
#define DIVISIONS 5//整个客户区分成5*5=25个矩形区域
...
static BOOL fState[DIVISIONS][DIVISIONS];//状态数组,fState[x][y]记录第x行低y列的格子状态
static int cxBlock, cyBlock;//一个格子的宽度和高度
HDC hdc;
int x, y;//临时变量,作为下标遍历每个格子
PAINTSTRUCT ps;

尺寸变化消息

1
2
3
4
case WM_SIZE:
cxBlock = LOWORD(lParam) / DIVISIONS;//lParam携带的是当前客户区大小,cxBlocks计算的是平均每个矩形的宽度
cyBlock = HIWORD(lParam) / DIVISIONS;//平均每个矩形的高度
return 0;

尽量用整个客户区打印所有方格

左键单击消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_LBUTTONDOWN:
x = LOWORD(lParam) / cxBlock;//鼠标的横坐标落在哪一列
y = HIWORD(lParam) / cyBlock;//鼠标的纵坐标落在哪一行
if (x < DIVISIONS && y < DIVISIONS) {//x,y都在合法范围之内
fState[x][y] ^= 1;//修改fState[x][y]的状态,1变0,0变1
rect.left = x * cxBlock;
rect.top = y * cyBlock;
rect.right = (x + 1) * cxBlock;
rect.bottom = (y + 1) * cyBlock;
//InvalidateRect(hwnd, &rect, TRUE);//rect对应区域失效
InvalidateRect(hwnd, NULL, TRUE);//全区域失效重绘
}

else {
MessageBeep(0);
}
return 0;

绘图消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
//HBRUSH hBrushBlack=(HBRUSH)
for (x = 0; x < DIVISIONS; ++x) {//遍历每个矩形区域
for (y = 0; y < DIVISIONS; ++y) {
if (fState[x][y]) {//1则表示这个格子是按下的状态,刷成灰色
SelectObject(hdc, (HBRUSH)GetStockObject(LTGRAY_BRUSH));
}
else {//0则表示这个格子没有按下,刷成白色
SelectObject(hdc, (HBRUSH)GetStockObject(WHITE_BRUSH));
}
Rectangle(hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock);//每个区域绘制矩形

}

}
EndPaint(hwnd, &ps);
return 0;

键盘模仿鼠标

使用方向键移动鼠标光标.使用Enter确认按下

这样即使计算机没有连接鼠标也能使用键盘模拟鼠标动作

比如windows桌面上如果有一个图标是高亮的,那么右方向键会使同行右侧的图标高亮,可以用隐藏光标,然后捕获位置进行击中测试,决定高亮哪个图标.

显示计数

书上扯了大半天,实际上就说了一个有用的东西

1
2
3
int ShowCursor(
[in] BOOL bShow
);

当bShow为False则不显示鼠标光标

当bShow为True则显示鼠标光标

指针位置

不管有没有接鼠标,鼠标指针都是存在的,一般开机时位于屏幕正中间.即使不使用鼠标移动指针位置,也可以使用键盘做到

1
2
3
4
5
6
7
BOOL GetCursorPos(
[out] LPPOINT lpPoint//lpPoint承接返回值,指针位置的坐标结构体
);
BOOL SetCursorPos(
[in] int X,//设置指针位置(X,Y)
[in] int Y
);

两个函数中涉及到的坐标都是屏幕坐标,如果需要客户区坐标,可以使用坐标转换函数

1
2
3
4
5
6
7
8
BOOL ScreenToClient(
[in] HWND hWnd,
LPPOINT lpPoint
);
BOOL ClientToScreen(
[in] HWND hWnd,
[in, out] LPPOINT lpPoint
);

使用GetCursorPos并且用ScreenToClient转换得到的指针位置和鼠标消息中的指针位置不同

前者是啥时候调用函数啥时候取得指针位置,后者指针位置是产生该条消息时指针的位置

击中测试

变量定义

1
2
3
4
5
6
7
8
9
#define DIVISIONS 5//客户区划分为5*5=25个区域
...
static int fState[DIVISIONS][DIVISIONS];//记录每个格子的状态
static int cxBlock, cyBlock;//每个格子的宽度和高度
HDC hdc;
int x, y;//临时变量,用来遍历fState
PAINTSTRUCT ps;//BeginPaint和EndPaint需要使用
POINT point;记录鼠标位置

窗口焦点信息

1
2
3
4
5
6
case WM_SETFOCUS://获得焦点
ShowCursor(TRUE);
return 0;
case WM_KILLFOCUS://失去焦点
ShowCursor(FALSE);
return 0;

当窗口获得焦点的时候显示光标,失去焦点的时候隐藏光标

尺寸调整信息

1
2
3
4
case WM_SIZE://客户区重新计算块大小
cxBlock = LOWORD(lParam) / DIVISIONS;
cyBlock = HIWORD(lParam) / DIVISIONS;
return 0;

当客户区尺寸发生变化的时候调整区块的大小

使得5*5个区块尽量占满整个客户区

虚拟键信息

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
case WM_KEYDOWN://虚拟键按下
GetCursorPos(&point);//获取当前鼠标位置
ScreenToClient(hWnd, &point);//转换屏幕坐标为客户区坐标
x = max(0, min(DIVISIONS - 1, point.x / cxBlock));//计算当前光标所在行
y = max(0, min(DIVISIONS - 1, point.y / cyBlock));//计算当前光标所在列
switch (wParam) {
case VK_UP://方向键上键
--y;//y的单位是列,上键按下之后纵坐标应该上移一个格子的高度
break;
case VK_DOWN:
++y;
break;
case VK_LEFT:
--x;
break;
case VK_HOME://Home键,光标回到左上角
x = y = 0;
break;
case VK_RIGHT:
++x;
break;
case VK_END:
x = y = DIVISIONS - 1;//End键,光标跳到右下格
break;
case VK_RETURN://回车和空格的作用相同,都相当于在当前格的左上角按下鼠标左键
case VK_SPACE:
SendMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG(x * cxBlock, y * cyBlock));//通知
break;
}
x = (x + DIVISIONS) % DIVISIONS;//计算当前指向方格
y = (y + DIVISIONS) % DIVISIONS;
point.x = x * cxBlock + cxBlock / 2;//光标放在这个格的正中间位置
point.y = y * cyBlock + cyBlock / 2;
ClientToScreen(hWnd, &point);//转换坐标
SetCursorPos(point.x, point.y);//设置新光标位置
return 0;

使用子窗口

每个子窗口都有自己的句柄,客户区,窗口过程函数.

多个子窗口将整个客户区划分成几个小的矩形区域

对于子窗口的鼠标消息,lParam参数包含相对于该子窗口左上角的坐标

本来我们的程序中使用了一个fState[DIVISIONS][DIVISIONS]二维数组保存每个区块的状态,整个程序就一个窗口过程,它遍历打印每个窗口的状态.

现在使用子窗口,使得每个区块成为一个子窗口,每个子窗口自己处理发生在自己身上的鼠标键盘动作

注册父窗口类

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
static TCHAR szAppName[] = TEXT("Checker4");//将要作为父窗口类名
HWND hwnd;
MSG msg;
WNDCLASS wndclass;

wndclass.style = CS_HREDRAW | CS_VREDRAW;//填写父窗口类信息
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL,
IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL,
IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;

if (!RegisterClass(&wndclass))//注册父窗口类
{
MessageBox(NULL, TEXT("Program requires Windows NT!"),
szAppName,
MB_ICONERROR);
return 0;
}

注册子窗口类

1
2
3
4
5
6
wndclass.lpfnWndProc = ChildWndProc;//修改一下wndclass的部分信息,填写子窗口类信息
wndclass.cbWndExtra = sizeof(long);
wndclass.hIcon = NULL;
wndclass.lpszClassName = szChildClass;//此处绑定了szChildClass字符串作为子窗口类的索引值

RegisterClass(&wndclass);//注册子窗口类

此处注册子窗口类,但是并不在winmain函数中创建实例,而是当父窗口起来之后,在其WM_CREATE消息处理中创建25个szChildClass指向的子窗口实例

创建父窗口实例,显示父窗口消息循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hwnd = CreateWindow(szAppName, TEXT("Checker4 Mouse Hit-Test Demo"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);

while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;

父窗口过程WndProc

变量定义

1
2
static HWND hwndChild[DIVISIONS][DIVISIONS];//子窗口句柄数组,hwndChild[x][y]为第x行第y列子窗口的句柄
int cxBlock, cyBlock, x, y;//cxBlock每个子窗口的尺寸,x遍历子窗口数组使用的下标

区分HWND句柄和HINSTANCE句柄

整个win32程序只有一个引用程序句柄HINSTANCE,

着一个程序可以有很多个窗口,每个窗口都有一个独一无二的窗口句柄HWND

窗口创建消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case WM_CREATE://主窗口创建消息,此时为创建子窗口的最佳时机
for (x = 0; x < DIVISIONS; x++)//遍历创建每一个子窗口
for (y = 0; y < DIVISIONS; y++)
hwndChild[x][y] =
CreateWindow(
szChildClass, //子窗口名
NULL,//子窗口标题为空
WS_CHILDWINDOW | WS_VISIBLE,//子窗口风格
0, 0, 0, 0,//子窗口坐标(相对于父窗口客户区左上角),初始位置
hwnd, //父窗口句柄
(HMENU)(y << 8 | x),//子窗口的菜单句柄,作用是给父窗口提供索引,在GetDlgItem 获取子窗口句柄时有重要作用
(HINSTANCE) GetWindowLong(hwnd, GWL_HINSTANCE),//从windows那里获取一个应用此程序实例句柄
NULL//通过WM_CREATE的lParam参数传递给子窗口的值的指针
);
return 0;
CreateWindow

创建父窗口实例时也使用了该函数,返回值是一个窗口实例的句柄

创建并显示窗口三个过程:

RegisterClass注册窗口类,该窗口类的类名作为句柄

CreateWindow创建窗口实例,可以使用刚才的窗口类名填充窗口的基本信息

ShowWindow显示窗口类

1
2
3
4
5
6
7
8
9
10
11
12
13
HWND CreateWindowA(
[in, optional] lpClassName,//子窗口使用的类名
[in, optional] lpWindowName,//子窗口名
[in] dwStyle,//子窗口风格
[in] x,//子窗口位置(相对于父窗口的客户区)
[in] y,
[in] nWidth,//子窗口的尺寸
[in] nHeight,
[in, optional] hWndParent,//父窗口的句柄
[in, optional] hMenu,//子窗口标识符
[in, optional] hInstance,//程序实例的句柄
[in, optional] lpParam//父窗口要传递给子窗口WM_CREATE消息,lParam参数的信息
);

调整尺寸消息

1
2
3
4
5
6
7
8
9
10
11
case WM_SIZE:
cxBlock = LOWORD(lParam) / DIVISIONS;//重新计算区块大小
cyBlock = HIWORD(lParam) / DIVISIONS;

for (x = 0; x < DIVISIONS; x++)
for (y = 0; y < DIVISIONS; y++)
MoveWindow(hwndChild[x][y],//调整子窗口的位置和大小
x * cxBlock, y * cyBlock,
cxBlock, cyBlock, TRUE);//大小是cxBlock宽,cyBlock高
return 0;

MoveWindow

对于子窗口来说,该函数中指定的坐标都是相对于父窗口客户区左上角的

对于非子窗口来说,该函数中的坐标是屏幕坐标

1
2
3
4
5
6
7
8
BOOL MoveWindow(
[in] HWND hWnd,//需要移动位置的窗口句柄,一般用于父窗口过程移动子窗口
[in] int X,
[in] int Y,
[in] int nWidth,//移动顺便设置尺寸
[in] int nHeight,
[in] BOOL bRepaint//是否重绘,TRUE则hWnd指向的窗口收到WM_PAINT消息
);

左键按下消息

1
2
3
case WM_LBUTTONDOWN:
MessageBeep(0);
return 0;

理论上25个子窗口尽量铺满父窗口的客户区,但是父窗口客户区的最右边和最下边可能有留白,因此当鼠标点击到这些地方的时候父窗口就会接到WM_LBUTTONDOWN消息

父窗口对这种消息的处理是发出一条蜂鸣声实际上是一个wav波形文件,还有一些系统定义好了的蜂鸣声,作用不大不展开了

获得焦点消息

1
2
3
case WM_SETFOCUS://父窗口获取焦点之后通知它上次获得焦点的子窗口继续获得焦点
SetFocus(GetDlgItem(hwnd, idFocus));//通知哪一个子窗口获得焦点,全局变量idFocus在WM_KEYDOWN被设置
return 0;

父窗口获得焦点之后,应该把最后一次获得焦点的子窗口作为焦点窗口

但是处理本消息时并没有体现获得idFocus,原因是该全局变量idFocus在WM_KEYDOWN中更新,显然没有按下键盘,通过鼠标点选也可以获得焦点,获得焦点的时候就需要指定让子窗口获得焦点

而现在父窗口掌握着25个子窗口句柄,应该怎么把焦点交给其中之一的子窗口呢?

1
2
3
4
HWND GetDlgItem(
[in, optional] HWND hDlg,//父窗口句柄
[in] int nIDDlgItem//子窗口的索引值
);

这个函数的作用是,返回父窗口的一个子窗口的句柄

这里nIDDlgItem这个值是父窗口注册子窗口是在CreateWindow函数的hMenu上指定的,其中第x行第y列的子窗口是这样索引的

1
(HMENU)(y << 8 | x)

既然要获取子窗口的句柄,父窗口不是实例化子窗口时就维护了一个子窗口句柄数组吗?

为啥还要额外维护一个值托管这个句柄呢?

只有一个子窗口句柄数组不能知道最近获得焦点的子窗口是谁,因此idFocus就起到了一个寄存器的作用

虚拟键消息

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
case WM_KEYDOWN:
x = idFocus & 0xFF;//x保存原来的焦点位置(子窗口下标)
y = idFocus >> 8;//y保存原来的焦点位置
switch (wParam)
{
case VK_UP:
y--;//焦点上移
break;
case VK_DOWN:
y++;
break;
case VK_LEFT:
x--;
break;
case VK_RIGHT:
x++;
break;
case VK_HOME:
x = y = 0;
break;
case VK_END:
x = y = DIVISIONS - 1;
break;
x = (x + DIVISIONS) % DIVISIONS;
y = (y + DIVISIONS) % DIVISIONS;
idFocus = y << 8 | x;//调整新焦点
SetFocus(GetDlgItem(hwnd, idFocus));//设置新焦点
return 0;

这里计算x,y坐标的方式根HMENU参数的定义方式相反,互为逆运算

1
2
3
4
5
		x = idFocus & 0xFF;//x保存原来的焦点位置
y = idFocus >> 8;//y保存原来的焦点位置
...

idFocus = y << 8 | x;//调整新焦点

WM_KEYDOWN消息执行完毕后立刻设置当前子窗口为焦点窗口

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

子窗口过程ChildWndProc

子窗口创建消息

1
2
3
case WM_CREATE:
SetWindowLong(hwnd, 0, 0); // on/off flag ,刚创建时本窗口的开关状态置0
return 0;

修改窗口的一个属性

1
2
3
4
5
LONG SetWindowLongA(
[in] HWND hWnd,//窗口句柄
[in] int nIndex,//指定要修改的窗口属性
[in] LONG dwNewLong//该窗口属性的新值
);

例子中修改的是下标为0的属性,对应的宏定义是DWL_MSGRESULT(0)

设置对话框过程的返回值,设置成了0.

实际上子窗口也不需要把这个值返回给父窗口看,它自己就可以决定把自己绘制成什么颜色

因此只是借用了一个线程的窗口属性来放置自己应该是按下还是起来的状态

子窗口键鼠消息

由于父窗口中会主动将焦点下放到子窗口,因此焦点子窗口可获取键鼠的输入.

父窗口只能等子窗口吃完了然后吃剩下的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_KEYDOWN:
// Send most key presses to the parent window

if (wParam != VK_RETURN && wParam != VK_SPACE)//子窗口只负责拦截并处理空格回车消息,其他键盘消息丢给父窗口
{
SendMessage(GetParent(hwnd), message, wParam, lParam);//其他键盘消息传递给父窗口
return 0;
}
// For Return and Space, fall through to toggle the square
//如果是空格和回车则相当于鼠标左键按下,一起处理
case WM_LBUTTONDOWN://左键单击按下
SetWindowLong(hwnd, 0, 1 ^ GetWindowLong(hwnd, 0));//子窗口开关状态置反
SetFocus(hwnd);//本子窗口获取焦点
InvalidateRect(hwnd, NULL, FALSE);//立刻重绘
return 0;
// For focus messages, invalidate the window for repaint

这里子窗口就处理里两个虚拟键消息,空格和回车,其他的消息通过

1
SendMessage(GetParent(hwnd), message, wParam, lParam);

转发给父窗口

空格,回车,左键单击一视同仁,首先本子窗口的属性置反

然后设置本窗口为焦点窗口

然后使窗口无效,导致重绘

子窗口获取/失去焦点消息

1
2
3
4
5
6
7
case WM_SETFOCUS:
idFocus = GetWindowLong(hwnd, GWL_ID);
// Fall through
case WM_KILLFOCUS:
InvalidateRect(hwnd, NULL, TRUE);//失效重绘,因为子窗口获得焦点的时候会有绘制方框提示,因此失去焦点时应当不再提示
return 0;

GetWindowLong 获取窗口属性

由于窗口属性都是LONG类型的值,因此该函数取名"GetWindowLong"

1
2
3
4
LONG GetWindowLongA(
[in] HWND hWnd,//指定要获取信息的窗口句柄
[in] int nIndex//指定要获取该窗口的哪个属性
);
Value Meaning
GWL_EXSTYLE-20 获取窗口实例的拓展风格,即CreateWindow函数指定的dwExStyle
GWL_HINSTANCE-6 获取应用程序句柄
GWL_HWNDPARENT-8 获取父窗口句柄
GWL_ID-12 获取本窗口的索引值,即CreateWindow函数指定的HMENU值
GWL_STYLE-16 获取窗口实例的风格,这个风格就是CreateWindow指定的dwStyle
GWL_USERDATA-21 Retrieves the user data associated with the window. This data is intended for use by the application that created the window. Its value is initially zero.
GWL_WNDPROC-4 Retrieves the address of the window procedure, or a handle representing the address of the window procedure. You must use the CallWindowProc function to call the window procedure.

The following values are also available when the hWnd parameter identifies a dialog box.

Value Meaning
DWL_DLGPROCDWLP_MSGRESULT + sizeof(LRESULT) Retrieves the address of the dialog box procedure, or a handle representing the address of the dialog box procedure. You must use the CallWindowProc function to call the dialog box procedure.
DWL_MSGRESULT0 Retrieves the return value of a message processed in the dialog box procedure.
DWL_USERDWLP_DLGPROC + sizeof(DLGPROC) Retrieves extra information private to the application, such as handles or pointers.

例子中获取的是子窗口的HMENU索引值

1
idFocus = GetWindowLong(hwnd, GWL_ID);

这句的意思就是子窗口将idFocus当前焦点窗口寄存器设置为子窗口自己

当子窗口失去焦点的时候需要通知失效重绘,其原因是WM_PAINT中获得焦点的子窗口会多绘制一些提示信息,那么当它失去焦点的时候就得擦除提示信息了

image-20220814172257251

子窗口绘图消息

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
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);

GetClientRect(hwnd, &rect);
Rectangle(hdc, 0, 0, rect.right, rect.bottom);//绘制边框
// Draw the "x" mark
if (GetWindowLong(hwnd, 0))//通过0号属性观察本窗口应该是按下还是起来的状态,
{//如果应该按下则本子窗口画出对角线
MoveToEx(hdc, 0, 0, NULL);
LineTo(hdc, rect.right, rect.bottom);
MoveToEx(hdc, 0, rect.bottom, NULL);
LineTo(hdc, rect.right, 0);
}
// Draw the "focus" rectangle

if (hwnd == GetFocus())//如果当前子窗口正在获得焦点,那么额外画出提示信息
{
rect.left += rect.right / 10;//设置内方框信息
rect.right -= rect.left;
rect.top += rect.bottom / 10;
rect.bottom -= rect.top;
SelectObject(hdc, GetStockObject(NULL_BRUSH));//不使用画刷
SelectObject(hdc, CreatePen(PS_DASH, 0, 0));//画笔改成虚线模式
Rectangle(hdc, rect.left, rect.top, rect.right,rect.bottom);
DeleteObject(SelectObject(hdc, GetStockObject(BLACK_PEN)));//删除刚才创建的逻辑画笔实例
}
EndPaint(hwnd, &ps);
return 0;

捕获鼠标

书上举了一个例子,一个绘图程序,假如要绘制一个矩形,鼠标左键按下之后确定矩形一个点,不松开拖着鼠标移动则实时绘制矩形边框,(好像这个边框叫做橡皮线)

当鼠标左键松开时确定整个矩形,此时填充矩形表明完成绘制

例程

绘制边框橡皮线函数

1
2
3
4
5
6
7
8
9
void DrawBoxOutline(HWND hwnd, POINT ptBeg, POINT ptEnd)//正常情况下客户区内松开左键时调用,填充矩形
{
HDC hdc;
hdc = GetDC(hwnd);
SetROP2(hdc, R2_NOT);//该函数的作用是,只要是绘图,当前背景色取反,原来是白板,取反得黑,交替使用绘图函数会呈现一白一黑
SelectObject(hdc, GetStockObject(NULL_BRUSH));//设置画笔无色,用ROP2反色下一次画出黑色
Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y);
ReleaseDC(hwnd, hdc);
}

ptBeg和ptEnd都是相对于hwnd的客户区而言的,意思是在hwnd中绘制一个左上角ptBeg到右下角的ptEnd矩形边框

SetROP2

1
2
3
4
int SetROP2(
[in] HDC hdc,
[in] int rop2//样式
);

The SetROP2 function sets the current foreground mix mode. GDI uses the foreground mix mode to combine pens and interiors of filled objects with the colors already on the screen. The foreground mix mode defines how colors from the brush or pen and the colors in the existing image are to be combined.

SetROP2函数设置当前前景的混合模式.

GDI使用前景混合模式,作用是将画笔画刷的行为和先前已有的颜色结合起来.也就是说本次绘画会影响之前的绘画,rop2参数指定怎么个影响方法,是将像素点的颜色异或还是按位与还是直接擦除先前颜色等等

前景和背景是相反的,背景在正文图层的下面,前景在正文图层的上面

rop2的可选值:

Mix mode Meaning
R2_BLACK 直接画黑,不考虑很多,不使用画笔颜色,相当于设置了一个寂寞
R2_COPYPEN 继续使用画笔颜色,相当于设置了一个寂寞
R2_MASKNOTPEN 原屏幕颜色和画笔反色的结合
R2_MASKPEN Pixel is a combination of the colors common to both the pen and the screen.
R2_MASKPENNOT Pixel is a combination of the colors common to both the pen and the inverse of the screen.
R2_MERGENOTPEN Pixel is a combination of the screen color and the inverse of the pen color.
R2_MERGEPEN Pixel is a combination of the pen color and the screen color.
R2_MERGEPENNOT Pixel is a combination of the pen color and the inverse of the screen color.
R2_NOP 啥也不改变,更是个寂寞
R2_NOT 和原屏幕颜色相反
R2_NOTCOPYPEN 笔的反色
R2_NOTMASKPEN Pixel is the inverse of the R2_MASKPEN color.
R2_NOTMERGEPEN Pixel is the inverse of the R2_MERGEPEN color.
R2_NOTXORPEN Pixel is the inverse of the R2_XORPEN color.
R2_WHITE Pixel is always 1.
R2_XORPEN Pixel is a combination of the colors in the pen and in the screen, but not in both.

左键按下消息

1
2
3
4
5
6
7
8
9
10
11
12
case WM_LBUTTONDOWN:
ptBeg.x = ptEnd.x = LOWORD(lParam);//ptBeg获取当前鼠标位置
ptBeg.y = ptEnd.y = HIWORD(lParam);

DrawBoxOutline(hwnd, ptBeg, ptEnd);//描矩形边.使用ptBeg,ptEnd指定主对角线的矩形

SetCursor(LoadCursor(NULL, IDC_CROSS));//左键按下后进入绘制状态,鼠标变成十字提示绘图

fBlocking = TRUE;//一个flag,表征鼠标是否一直按下,这个状态会被Esc键修改,表示终止绘图
SetCapture(hwnd);

return 0;

左键按下时就已经开始绘图了,首先绘制一个点,即左键按下时的鼠标位置,

fBlocking变量用来记录绘图过程中有没有被Esc打断过.

SetCapture(hwnd)意思是从调用该函数开始,所有鼠标动作,包括不在本客户区的鼠标动作,全都被本程序捕获.这种状态需要到ReleaseCapture解出

Set/ReleaseCapture
1
2
SetCapture(hwnd);//此函数执行之后所有的鼠标动作将被hwnd指向的窗口捕获
ReleaseCapture();//直到本函数执行之后鼠标动作才会恢复正常

鼠标移动消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case WM_MOUSEMOVE:
if (fBlocking)
{
SetCursor(LoadCursor(NULL, IDC_CROSS));

DrawBoxOutline(hwnd, ptBeg, ptEnd);

ptEnd.x = LOWORD(lParam);
ptEnd.y = HIWORD(lParam);

DrawBoxOutline(hwnd, ptBeg, ptEnd);//这里矩形边框画了两次,是因为后来这一次会更新前面那一次,由于SetROP2已经设置了黑白交替绘画,
//当前一次是黑笔是,后一次是白笔就擦除了前面的绘制
//如此做到只能看到一个实时边框,否则只绘制一次会导致很多重影
}
return 0;

由于左键起来之后会进行结算,因此绘图时的鼠标移动是压着左键移动的

首先判断了fBlocking的状态,如果被Esc打断则直接退出,不做处理

这里调用了两次DrawBoxOutline,其作用是:

由于SetROP2(hdc, R2_NOT)这个设置,会导致相邻两次绘图使用的颜色相反

如果本次使用黑色,那么下一次就使用白色

这样交替绘制的意义是:刚用黑色绘制出边框,接着逻辑上擦除它,但是在屏幕上不显示擦除,这就是压着黑笔停下时仍然能看到过期的客户区上有边框线.当画笔再次移动时,先前的边框已经被逻辑擦除,此时再画新线保证只有新线,不会有重影.这就实现了橡皮线的效果

这里两次调用DrawBoxOutline,头一次的ptEnd没有被修改,也就是上一次绘图使用的ptEnd,那么重绘这个矩形,相当于擦除了上一次的绘制

左键起来消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_LBUTTONUP:
if (fBlocking)//如果fBlocking==1说明左键按下没有起来过并且没有被Esc中断过
{
DrawBoxOutline(hwnd, ptBeg, ptEnd);//这里只调用了一次,是因为这一次要固定边框位置了,直接描黑

ptBoxBeg = ptBeg;
ptBoxEnd.x = LOWORD(lParam);//准备好需要填充的矩形范围
ptBoxEnd.y = HIWORD(lParam);

ReleaseCapture();
SetCursor(LoadCursor(NULL, IDC_ARROW));//放下屠刀之后,鼠标变成了斜向箭头样式
fBlocking = FALSE;//关闭中断标志,为下一次绘图做准备
fValidBox = TRUE;//设置填充区域有效,可以填充,为WM_PAINT做准备

InvalidateRect(hwnd, NULL, TRUE);
}
return 0;

首先判断fBlocking状态,如果中途被Esc打断过则不做处理

调用一次DrawBoxOutline擦除最后一次WM_MOUSEMOVE留下的边框.但是最后一次WM_MOUSEMOVE和WM_LBUTTONUP的到达时间非常接近,拉不开差距,因此这个函数调用与否意义不大,除非电脑很卡

释放对鼠标的捕获状态,程序对于客户区以外的鼠标动作不再处理

鼠标样式还原为斜向箭头

重置中断标志,设置绘图标志有效,提醒WM_PAINT应该绘制填充矩形了

Esc中断消息

1
2
3
4
5
6
7
8
9
10
case WM_CHAR:
if (fBlocking & wParam == '\x1B') // i.e., Escape //按下Esc终止绘制矩形,即使左键正在按下
{
DrawBoxOutline(hwnd, ptBeg, ptEnd);

SetCursor(LoadCursor(NULL, IDC_ARROW));

fBlocking = FALSE;//Esc中止逻辑修改了一个fBlocking,就可以让程序知道左键按下起来之间有没有被中断过
}
return 0;

对于该消息,例子采用的是处理字符消息,而不是处理虚拟键消息

如果出现Esc消息并且fBlocking表明正在绘图,那么设置fBlocking绘图无效

此后由于WM_MOUSEMOVE需要判断fBlocking正在绘图才继续绘制边框,因此Esc出现之后矩形边框橡皮线立刻消失

然而这种绘制失效的状态需要等到左键起来才能完全恢复

WM_LBUTTONUP也会先判断fBlocking是否还有效.无效则啥也不处理

此时所有标志都恢复原状(fValidBox压根没有被改变过,fBlocking被Esc重置为假)

绘图消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
if (fValidBox)//这个值只能通过鼠标左键起来修改,表明确实有过画图
{
SelectObject(hdc, GetStockObject(BLACK_BRUSH));
Rectangle(hdc, ptBoxBeg.x, ptBoxBeg.y,ptBoxEnd.x, ptBoxEnd.y);//填充
}

if (fBlocking)//fBlocking=1表明正在绘图,应当打印边框
{
SetROP2(hdc, R2_NOT);
SelectObject(hdc, GetStockObject(NULL_BRUSH));
Rectangle(hdc, ptBeg.x, ptBeg.y, ptEnd.x,ptEnd.y);
}

EndPaint(hwnd, &ps);
return 0;

fValidBox是LBUTTONUP准备好的绘制标志,如果绘画中途没有Esc中断则LBUTTONUP消息处理中,会把fValidBox置有效,提醒WM_PAINT应该画图了

修改画刷为黑色画刷然后填充最后确定的矩形

如果fBlocking为有效说明仍然没有确定矩形的另一个点,此时WM_PAINT也打印矩形边框橡皮线,然而是多次一句,因为WM_MOUSEMOVE已经把这件事干了.

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

鼠标滚动

鼠标滚动消息WM_MOUSEWHEEL

当鼠标滚轮滚动时,该消息将被发往焦点窗口,

参数意义:

wParam

高字表明滚动增量

低字表明同时按下的虚拟键

Value Meaning
MK_CONTROL0x0008 The CTRL key is down.
MK_LBUTTON0x0001 The left mouse button is down.
MK_MBUTTON0x0010 The middle mouse button is down.
MK_RBUTTON0x0002 The right mouse button is down.
MK_SHIFT0x0004 The SHIFT key is down.
MK_XBUTTON10x0020 The first X button is down.
MK_XBUTTON20x0040 The second X button is down.

lParam

低字表明此时鼠标位置横坐标,屏幕坐标

高字表明纵坐标

滚动增量

衡量滚得狠不狠的参数,如果一下子转了好几圈显然增量很大,如果只发生了很少的转动,那么这点增量几乎不能导致程序卷动

在控制面板中我们可以设置滚动灵敏度,相同的滚动增量,假设都转一圈,可能灵敏度高的可以滚动一整页,灵敏度低的滚了三行,这是怎么实现的呢?

用滚动增量除以一个灵敏度系数,不妨给这个量起名灵敏后增量(我乱起的)

比如滚动增量为120,除以3得到40就是灵敏后增量.

在程序中我们可以设置一个单位行增量,意思是多少滚动增量可以导致程序卷动一个行,比如说设置为40

那么当灵敏度系数为3,那么初始时的120滚动增量就可以得到40 的灵敏后增量,刚好可以卷动一行

如果灵敏度系数为1,则灵敏后增量就是120.可以卷动三行

这就体现出不同灵敏度的区别了

加装滚动动作的Sysmets程序

滚动相关变量

1
2
static int iDeltaPerLine, iAccumDelta;
ULONG ulScrollLines;

iDeltaPerLine即单位行增量

iAccumDelta表示先前的滚动效果的累加,可以理解为初始滚动量

ulScrollLines存放灵敏度系数

滚动消息

1
2
3
4
5
6
7
8
9
10
11
12
case WM_MOUSEWHEEL:
if (iDeltaPerLine == 0)break;
iAccumDelta += (short)HIWORD(wParam);//累计滚动量
while (iAccumDelta >= iDeltaPerLine) {//能滚就滚
SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);//滚一行
iAccumDelta -= iDeltaPerLine;
}
while (iAccumDelta <= -iDeltaPerLine) {//倒着能滚就滚
SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
iAccumDelta += iDeltaPerLine;
}
return 0;

windows SDK chapter 6 keyboard

键盘基础

who is using the keyboard?

正在使用键盘的窗口,称为有输入焦点的窗口

windows操作系统上处于最顶层的窗口往往是焦点窗口,然而存在两分屏看似并列的情况,实际上这时候可以从多方面判断哪个窗口是焦点窗口

比如从任务栏

image-20220812083837696

这里Typora上有高亮,就是焦点窗口,

windows终端,DEVC++,VisualStudio等等都是黑色的,不是焦点窗口

又如从标题栏

image-20220812083907654
左侧typora为焦点窗口,其标题栏为深色.

在word,wps等等文字编辑器中,是否获得焦点更加明显,只要光标在闪烁就意味着持有焦点

实际上窗口过程通过处理WM_SETFOCUS,WM_KILLFOCUS两个消息来判断自己有没有焦点

1
2
3
4
5
6
case WM_SETFOCUS:
cout<<"focused"<<endl;
return 0;
case WM_KILLFOCUS:
cout<<"left"<<endl;
return 0;
1
PS C:\Users\86135\desktop\myWin32> g++ main.cpp -O0 -o main -lkernel32 -lgdi32 -luser32 -m32
焦点在windows终端
焦点在the Hello Program

队列与同步

键盘动作由键盘驱动程序转化为格式化的消息之后,首先发往windows消息队列,不会直接发往焦点窗口

这是因为,有的键盘消息具有修改焦点窗口的功能,比如Alt+F4可关闭当前窗口.又比如win+r可以打开运行框,此时窗口焦点自动放在运行框上

windows需要先看一看键盘消息是针对当前焦点窗口的还是针对整个系统的.

如果发生了焦点转移,那么windows需要保证后续的相应键盘消息指向新的焦点窗口

击键消息

image-20220812085353287

其中系统键一般是Alt+其他键的组合键,比如Alt+Tab,切换任务窗口.

键按下之后如果不松开,会间隔一定时间(这个间隔在系统启动时BIOS中设置)之后进入连续输入状态(连续输入的速度也取决于BIOS中的设置),此时应用程序会收到一连串的WM_KEYDOWN或者WM_SYSKEYDOWN消息

系统键对于windows操作系统来说更加重要,应用程序一般忽略系统键信息,交给DefWindowProc处理.windows会处理所有Alt组合键信息

如果非得在应用程序中处理Alt消息,并且处理完成之后立刻返回,不调用DefWindowProc,那么系统键消息将会被应用程序截胡,不能发给操作系统.此时按下Alt+F4就无法关闭该窗口了

比如:

1
2
3
4
5
6
case WM_SYSKEYDOWN:
cout << "sysKey down" << endl;
return 0;
case WM_SYSKEYUP:
cout << "sysKey up" << endl;
return 0;

此时只会在终端上打印一下,但是只要时Alt键的消息都不会发往windows

当组合键中没有Alt时,不会产生系统键消息,顶多产生WM_KEYDOWN和WM_KEYUP,应用程序可以根据自己的兴趣选择处理其中的部分消息,如果应用程序不做处理,windows也不做处理

不管是系统键消息还是非系统键消息,不管是按下还是起来,所有的键盘消息都会伴随着wParam表示虚拟键代码,lParam包含本次击键的其他数据

虚拟键代码wParam

捕获WM_KEYDOWN或者WM_KEYUP消息只能说明非系统键被按下或者松开了,单凭着一个信息无法判断谁被按下或者松开了.wParam就提供了更多的细节

该案件消息是一个枚举类型,大多数都以VK_开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define VK_LBUTTON 0x01
#define VK_RBUTTON 0x02
#define VK_CANCEL 0x03
#define VK_MBUTTON 0x04
#define VK_XBUTTON1 0x05
#define VK_XBUTTON2 0x06
#define VK_BACK 0x08
#define VK_TAB 0x09
#define VK_CLEAR 0x0C
#define VK_RETURN 0x0D
#define VK_SHIFT 0x10
#define VK_CONTROL 0x11
#define VK_MENU 0x12
#define VK_PAUSE 0x13
...

前两个鼠标动作貌似通过处理不到

数字键(不是右侧数字键盘,是左侧主键盘上一长条的数字键)和字母键:

image-20220812092101119

虽然键盘可以接收到键盘上@#$%*这种字符的消息,但是这是对于英语键盘而言的,对于欧洲人来说欧元符号比美元符号更加重要,如果要写出地区无关的代码,那么我们需要和windows操作系统协作处理键盘消息,对于有意义的字符的处理,一般使用字符消息,不使用击键消息

微软键盘特有:

image-20220812092200252

需要给哪个键安排任务的时候就去查它的宏定义编码即可

其他消息lParam

image-20220812094137721

重复计数Repeat Count

低16位为重复计数,当一个键按下一直不松开时,应用程序会接收到一大串WM_KEYDOWN消息.如果窗口过程的处理速度跟不上,那么windows会把多个WM_KEYDOWN消息合并成一条消息,然后修改其lParam的低16位,决定这条消息重复了多少次

在比较卡顿的电脑上使用word有过这种体验,一直按着某个键不放,计算机可能不会实时跟着显示字符,但是一段时间后呼哧出来一摊这个字符

OEM扫描码

历史古董了,现在几乎不用了

拓展键标记Extended Key Flag

对于IBM拓展键盘才有用,一般的键盘上已经有足够多的键用了,不需要拓展了

内容代码Context Code

如果Alt键被按下,则相应的键盘消息中该值为1

先前状态Previous Key State

如果先前改键是松开的则该值为0

如果先前该键是按下的则该值为1

这个值可以用来去除重复输入,比如老年人动作缓慢,本来就只想输入一个A结果按下A忘了松开了 ,可以判断先前该键的状态,如果也是按下的则抛弃

1
2
3
4
case WM_KEYDOWN:
if((lParam>>30)&1){//lParam第31位是键先前状态,右移30位之后成为最低位,和1按位与只保留该位
return 0;
}

转换状态Transition State

键正在被按下则转换状态为0

键正在被释放则转换状态为1

转义状态

转义键:Shift,Alt,Ctrl

切换键:Caps Lock,Num Lock,Scroll Lock

区分主键盘上大小写输入就要看这些键的状态

考虑如何判断一套组合键都有谁按下了呢?比如Ctrl+F,首先按下的是Ctrl,然后按下F,但是按下F的时候引用程序已经处理过了Ctrl,现在只知道按下了F.这样看来是不是还得开一些变量记录刚才按下了谁呢?当Ctrl松开的时候这个变量置零,当Ctrl按下的时候这个变量置1.

windows操作系统确实是按照这个思想做的,它给我们代劳了.我们要判断Ctrl的状态,只需要GetKeyState(VK_CONTROL);就可以获取windows帮我们记录好的Ctrl状态了

1
2
3
SHORT GetKeyState(
[in] int nVirtKey
);

当需要查询状态的键处于按下时,该函数返回负值.否则最低位置1

可以通过是否小于0或者和1按位与判断状态

1
2
3
4
5
6
7
8
9
if(wParam<=0x5A&&wParam>=0x41){//首先判断是不是英文字母
if(GetKeyState(VK_CAPITAL)){
szBuffer[cnt_keys]=wParam;//大写字母添加进入缓冲区
}
else{
szBuffer[cnt_keys]=wParam-'A'+'a';//小写字母添加进入缓冲区
}
++cnt_keys;
}

应用

给滚动条添加键盘动作:

按下PgUp往前翻页,按下PgDn往后翻页

按上下键滚动一行

按左右键左右滚动一个字符的宽度

实际上直接发送信息处理滚动条消息即可

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
case WM_KEYDOWN:
switch (wParam) {
case VK_HOME:
SendMessage(hwnd, WM_VSCROLL, SB_BOTTOM, 0);
break;
case VK_END:
SendMessage(hwnd, WM_VSCROLL, SB_TOP, 0);
break;
case VK_PRIOR:
SendMessage(hwnd, WM_VSCROLL, SB_PAGEUP, 0);
break;
case VK_NEXT:
SendMessage(hwnd, WM_VSCROLL, SB_PAGEDOWN, 0);
case VK_UP:
SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
break;
case VK_DOWN:
SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
break;
case VK_RIGHT:
SendMessage(hwnd, WM_HSCROLL, SB_LINERIGHT, 0);
break;
case VK_LEFT:
SendMessage(hwnd, WM_HSCROLL, SB_LINELEFT, 0);
break;
}

字符消息

再看消息循环

1
2
3
4
5
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

在主函数的最后,返回之前,像定式一样必须要有一个消息循环.

其中GetMessage从应用程序的消息队列中,获取一条新消息,用msg承载该消息

DispatchMessage将该消息分拣交给相应窗口

TranslageMessage将可打印字符的击键消息转换为字符消息,然后将该字符消息放入应用程序的消息队列

可打印字符包括英文字母,阿拉伯数字,标点符号,运算符号等等

不包括Ctrl,Shift,Insert,Delete,Alt

字符消息的种类:

image-20220812105605047

死字符只对需要重音符号的语言键盘有用,比如德语.可以忽略

应用程序主要就处理WM_CHAR消息,该消息从WM_KEYDOWN翻译而来.

WM_SYSCHAR消息从WM_SYSKEYDOWN翻译而来

参数意义

对于字符消息,lParam参数的含义和击键消息相同

image-20220812201421640

wParam是ANSI或者Unicode编码的字符码,这一点和击键消息中不同

具体是用的ANSI还是Unicode编码,要看注册窗口类的时候调用的是RegisterClassA还是RegisterClassW

如果使用RegisterClass,最近的windows操作系统上会被宏定义为RegisterClassW.因为Windows 2000之后Unicode标识符就被定义了

1
2
3
4
5
#ifdef UNICODE
#define RegisterClass RegisterClassW
#else
#define RegisterClass RegisterClassA
#endif // !UNICODE

先来后到

当字符键盘比如'A'按下后,对于应用程序的消息队列

首先收到的是'A'的击键消息WM_KEYDOWN,虚拟键代码wParam=0x41

然后收到的是'A'的字符消息WM_CHAR,字符编码wParam=0x61

然后'A'松开时收到击键消息WM_KEYUP,虚拟键代码wParam=0x41

各司其职

对于功能键比如Ctrl,Shift等等,作为虚拟键,需要处理WM_KEYDOWN消息

对于可打印字符就需要处理字符消息WM_CHAR

Tab,回车,空格,Esc作为控制字符也处理WM_CHAR

image-20220812203020207
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case WM_CHAR:
switch (wParam)
{
case '\b':
cout << "space down" << endl;
break;
case '\t':
cout << "Tab down" << endl;
break;
case '\n':
cout << "Ctrl+Enter down" << endl;
break;
case '\r':
cout << "Enter down" << endl;
break;
}
return 0;

应用

windows程序设计在此给出了一个例子,记录键盘动作并且输出到屏幕

分析其过程函数WndProc

变量定义

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
	static int cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar;
//cxClientMax为客户区最大宽度,根据该值决定保存显示哪一部分消息
//cxClient是客户区宽度
static int cLinesMax, cLines;//当前客户区最大能打印多少行,已经使用了多少行
static PMSG pmsg;//消息结构体
static RECT rectScroll;//客户区矩形
static TCHAR szTop[] = TEXT("Message Key Char ") TEXT("Repeat Scan Ext ALT Prev Tran");//顶部栏目
static TCHAR szUnd[] = TEXT("_______ ___ ____ ") TEXT("______ ____ ___ ___ ____ ____");//下划线
static TCHAR szFormat[2][100] = {//格式化字符串,用于szTop和szUnd的打印格式
TEXT("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"),
TEXT("%-13s 0x%04X%1s%c %6u %4d %3s %3s %4s %4s")
};

static TCHAR szYes[] = TEXT("Yes");
static TCHAR szNo[] = TEXT("No");
static TCHAR szDown[] = TEXT("Down");
static TCHAR szUp[] = TEXT("Up");
static TCHAR szMessage[8][100] = {//八种键盘消息
TEXT("WM_KEYDOWN"), TEXT("WM_KEYUP"),
TEXT("WM_CHAR"), TEXT("WM_DEADCHAR"),
TEXT("WM_SYSKEYDOWN"),TEXT("WM_SYSKEYUP"),
TEXT("WM_SYSCHAR"), TEXT("WM_SYSDEADCHAR")
};
HDC hdc;//设备环境句柄
int i, iType;//循环变量,
PAINTSTRUCT ps;
TCHAR szBuffer[128], szKeyName[32];
TEXTMETRIC tm;//字符信息结构体

pmsg

存放消息的结构体

1
2
3
4
5
6
7
8
9
10
11
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

其前四个参数和WndProc窗口过程函数相同

1
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);

该结构体的作用就是完整记录一条消息,甚至连消息产生时间time都有记录

POINT pt记录的是发生该消息时鼠标的位置(相对于整块屏幕的坐标)

ps

记录绘画信息的结构体

1
2
3
4
5
6
7
8
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;
hdc

设备环境句柄

fErase

表明背景是否擦除,非0则擦除

rcPaint

需要重绘的矩形范围

剩下三个成员尚未使用

窗口创建和变形消息

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
case WM_CREATE:
case WM_DISPLAYCHANGE://窗口最大化还有普通状态变化时获取该消息
// Get maximum size of client area
cxClientMax = GetSystemMetrics(SM_CXMAXIMIZED);//顶层窗口最大化时的宽度(像素)
cyClientMax = GetSystemMetrics(SM_CYMAXIMIZED);
// Get character size for fixed-pitch font
hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));//使用系统等宽字体
GetTextMetrics(hdc, &tm);//获取系统等宽字体信息
cxChar = tm.tmAveCharWidth;//字符宽度,列宽度
cyChar = tm.tmHeight+tm.tmExternalLeading;//行高度
ReleaseDC(hwnd, hdc);
// Allocate memory for display lines
if (pmsg)//如果pmsg非NULL则表明先前已经给他分配过堆空间,那么本次需要重新分配,先把以前的扬了
free(pmsg);
cLinesMax = cyClientMax / cyChar;//计算行数
pmsg = (PMSG)malloc(cLinesMax * sizeof(MSG));//根据当前客户区大小决定pmsg数组大小
cLines = 0;//已使用行数清零
// fall through
//此处没有返回直接继续执行case WM_SIZE标签中的内容
case WM_SIZE:
if (message == WM_SIZE)//由于能够到达此块的不止WM_SIZE消息,还有可能时WM_CREATE和WM_DISPLAYCHANGE
{
cxClient = LOWORD(lParam);//只有窗口尺寸发生变化时才重新获取客户区尺寸
cyClient = HIWORD(lParam);
}
// Calculate scrolling rectangle
//滚动矩形的
rectScroll.left = 0;
rectScroll.right = cxClient;
rectScroll.top = cyChar;//滚动矩形起点是第一行,第0行用于打印栏目常量
rectScroll.bottom = cyChar * (cyClient / cyChar);//滚动矩形的底是客户区最多能容纳的那一行
InvalidateRect(hwnd, NULL, TRUE);//一旦客户区尺寸变化则重绘整个客户区
return 0;

键盘消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	//所有的键盘动作,包括击键消息和字符消息还有系统键消息都记录下来
case WM_KEYDOWN://键盘按下
case WM_KEYUP://键盘起来
case WM_CHAR://字符消息
case WM_DEADCHAR://死字符消息
case WM_SYSKEYDOWN://系统键Alt按下
case WM_SYSKEYUP://系统键起来
case WM_SYSCHAR://系统字符消息
case WM_SYSDEADCHAR://系统死字符消息
// Rearrange storage array
for (i = cLinesMax - 1; i > 0; i--)
{
pmsg[i] = pmsg[i - 1];//新的键盘记录放到pmsg[0],历史消息都顺次后移一个
//如果消息总数超过了cLinesMax则丢弃早些时候到达的消息,实际上维护了一个队列
}
// Store new message
pmsg[0].hwnd = hwnd;//记录最新键盘消息
pmsg[0].message = message;
pmsg[0].wParam = wParam;
pmsg[0].lParam = lParam;
cLines = min(cLines + 1, cLinesMax);//更新 "已使用行数" ,但是最大值不能超过最大行数
// Scroll up the display
ScrollWindow(hwnd, 0, -cyChar, &rectScroll, &rectScroll);//滚动rectScroll对应区域,实际上整个网上平移cyChar即一行的高度
break; // i.e., call DefWindowProc so Sys messages work

"i.e., call DefWindowProc so Sys messages work"翻译成人话:

"也就是说,调用DefWindowProc"处理系统键消息

最后这里用的是break不是return,意味着只是看看键盘消息是谁而不进行拦截,记录一下接着丢给系统做取舍,保证Alt系统消息能够被正确处理

绘图消息

前面的一切消息处理都是在给绘图消息的处理做准备

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
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
SetBkMode(hdc, TRANSPARENT);
TextOut(hdc, 0, 0, szTop, lstrlen(szTop));//打印最顶上第0行
TextOut(hdc, 0, 0, szUnd, lstrlen(szUnd));
for (i = 0; i < min(cLines, cyClient / cyChar - 1); i++)//一共打印cLines行,但是不能超过客户区总行数
{
iType = pmsg[i].message == WM_CHAR ||//如果是字符消息则iType=1否则为0
pmsg[i].message == WM_SYSCHAR ||
pmsg[i].message == WM_DEADCHAR ||
pmsg[i].message == WM_SYSDEADCHAR;
GetKeyNameText(pmsg[i].lParam, szKeyName,
sizeof(szKeyName) / sizeof(TCHAR));//获取表示键名的字符串
TextOut(hdc, 0, (cyClient / cyChar - 1 - i) * cyChar, szBuffer,
wsprintf(szBuffer, szFormat[iType],//根据szFormat[iType]格式化字符串进行打印,即区分字符消息和虚拟键消息
szMessage[pmsg[i].message - WM_KEYFIRST],///如此计算出来正好得到消息类型
pmsg[i].wParam,
(PTSTR)(iType ? TEXT(" ") : szKeyName),//如果是字符类型则打印空字符串否则打印键名
(TCHAR)(iType ? pmsg[i].wParam : ' '),//如果是字符类型则打印该字符,否则打印空串
LOWORD(pmsg[i].lParam),
HIWORD(pmsg[i].lParam) & 0xFF,
0x01000000 & pmsg[i].lParam ? szYes : szNo,
0x20000000 & pmsg[i].lParam ? szYes : szNo,
0x40000000 & pmsg[i].lParam ? szDown : szUp,
0x80000000 & pmsg[i].lParam ? szUp : szDown));
}
EndPaint(hwnd, &ps);
return 0;

其中szMessage[pmsg[i].message - WM_KEYFIRST]很巧妙

szMessage在WndProc一开始有定义

1
2
3
4
5
6
static TCHAR  szMessage[8][100] = {
TEXT("WM_KEYDOWN"), TEXT("WM_KEYUP"),
TEXT("WM_CHAR"), TEXT("WM_DEADCHAR"),
TEXT("WM_SYSKEYDOWN"),TEXT("WM_SYSKEYUP"),
TEXT("WM_SYSCHAR"), TEXT("WM_SYSDEADCHAR")
};
1
2
3
4
5
6
7
8
9
#define WM_KEYFIRST                     0x0100
#define WM_KEYDOWN 0x0100
#define WM_KEYUP 0x0101
#define WM_CHAR 0x0102
#define WM_DEADCHAR 0x0103
#define WM_SYSKEYDOWN 0x0104
#define WM_SYSKEYUP 0x0105
#define WM_SYSCHAR 0x0106
#define WM_SYSDEADCHAR 0x0107

WM_KEYFIRST表示WM消息的第一个,WM_KEYDOWN-WM_KEYFIRSST=0x100-0x100=0

正好对应szMessage下标为0的的成员TEXT("WM_KEYDOWN")

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

插入符号

向文本编辑器中输入文本时,当前输入位置会有一个方格或者竖线或者下划线闪烁,

这个闪烁的玩意儿叫做"插入符号(caret)",而不是"光标(cursor)",光标是指鼠标位置

windows终端中的插入符号

windowsAPI提供了插入符号的现成的函数,

在此之前我还以为这个闪烁效果得是行尾一个竖线|和空格交替重绘.客户区其他部位一直重绘.显然客户区重新打印一遍太耗时了

1
2
3
4
5
6
7
8
CreateCaret 建立与视窗有关的插入符号
SetCaretPos 在视窗中设定插入符号的位置
ShowCaret 显示插入符号
HideCaret 隐藏插入符号
DestroyCaret 撤消插入符号
GetCaretPos 获取当前插入符号位置
GetCaretBlinkTime 获取符号闪烁时间
SetCaretBlinkTime 设置符号闪烁时间

时机

插入符号的作用就是提示用户当前输入位置,显然这个正在被输入的窗口是焦点窗口

而一个窗口过程可能负责多个窗口的消息处理,那么插入符号的改变应该是窗口特定的,不应是窗口过程特定的

因此最好的处理时机是WM_SETFOCUS和WM_KILLFOCUS消息

在处理WM_SETFOCUS时调用CreateCaret,在处理WM_KILLFOCUS消息时调用DestoryCaret

创建插入符号之后并没有立刻输出到屏幕,需要再挑一个适当的时候调用ShowCaret显示它

在处理WM_PAINT时调用CreateCaret,在处理其他需要绘图的消息时调用HideCaret暂时隐藏插入符号.其他消息处理完成之后再调用CreateCaret重新显示插入符号

HideCaret的效果叠加,假设连续调用了10次HideCaret函数,那么就需要调用ShowCaret函数10次才可以把插入符号拽出来.

baby notepad

"弟弟军训完了,非要给我露一手"

书上在这里给出了一个例子,一个婴儿版的文本编辑器

分析一下它的窗口过程WndProc函数

宏定义

1
#define BUFFER(x,y) *(pBuffer + y * cxBuffer + x)

本定义用于取缓冲区的第x行第y列这个字符.

实际上用了一个一维数组模拟二维数组,BUFFER(x,y)相当于buffer[x][y]

变量定义

1
2
3
4
5
6
7
8
9
static DWORD dwCharSet = DEFAULT_CHARSET;//字符集标志
static int cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer,
xCaret, yCaret;
static TCHAR* pBuffer = NULL;
HDC hdc;
int x, y, i;
PAINTSTRUCT ps;
TEXTMETRIC tm;

字符集变换,窗口创建,尺寸变化消息

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
	case WM_INPUTLANGCHANGE:
dwCharSet = wParam;//字符集,和输入法有关,默认是DEFAULT_CHARSET
// fall through
case WM_CREATE:
hdc = GetDC(hwnd);
SelectObject(hdc, CreateFont(0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL));
//输入法改变或者窗口创建时都需要重新设置字符集,字符集是字体的一部分

GetTextMetrics(hdc, &tm);
cxChar = tm.tmAveCharWidth;//获取字符宽度
cyChar = tm.tmHeight;//字符高度

DeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));//使用系统字体
ReleaseDC(hwnd, hdc);
// fall through
case WM_SIZE:
// obtain window size in pixels
if (message == WM_SIZE)
{
cxClient = LOWORD(lParam);//更新客户区尺寸记录
cyClient = HIWORD(lParam);
}
// calculate window size in characters

cxBuffer = max(1, cxClient / cxChar);//横向最多打印cxClient/cxChar列
cyBuffer = max(1, cyClient / cyChar);//纵向最多打印cyClient/cyChar行

// allocate memory for buffer and clear it

if (pBuffer != NULL)
free(pBuffer);
pBuffer = (TCHAR*)malloc(cxBuffer * cyBuffer * sizeof(TCHAR));//pBuffer为需要打印到屏幕的缓冲区,其大小按照cxBuffer*cyBuffer分配

for (y = 0; y < cyBuffer; y++)
for (x = 0; x < cxBuffer; x++)
BUFFER(x, y) = ' ';//初始化buffer全为空格
// set caret to upper left corner
xCaret = 0;//初始插入字符位置(0,0)
yCaret = 0;

if (hwnd == GetFocus())//判断当前窗口是否为焦点窗口
SetCaretPos(xCaret * cxChar, yCaret * cyChar);//如果是已经是焦点窗口了则设置当前插入字符位置
InvalidateRect(hwnd, NULL, TRUE);//重绘整个客户区
return 0;

获取焦点消息

1
2
3
4
5
6
case WM_SETFOCUS:
// create and show the caret
CreateCaret(hwnd, NULL, cxChar, cyChar);//当前窗口获取焦点,此时创建插入字符
SetCaretPos(xCaret * cxChar, yCaret * cyChar);//设置好插入字符的位置,这个位置可以通过方向键等改变
ShowCaret(hwnd);//显示插入字符
return 0;

失去焦点消息

1
2
3
4
5
case WM_KILLFOCUS:
// hide and destroy the caret
HideCaret(hwnd);//失去焦点,隐藏插入符号
DestroyCaret();//删除插入符号
return 0;

键盘消息

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
case WM_KEYDOWN:
switch (wParam)
{
case VK_HOME://各种移动插入符号位置
xCaret = 0;
break;

case VK_END:
xCaret = cxBuffer - 1;
break;

case VK_PRIOR:
yCaret = 0;
break;

case VK_NEXT:
yCaret = cyBuffer - 1;
break;

case VK_LEFT:
xCaret = max(xCaret - 1, 0);//最左不能移出客户区
break;

case VK_RIGHT:
xCaret = min(xCaret + 1, cxBuffer - 1);//最右不能移出客户区
break;

case VK_UP:
yCaret = max(yCaret - 1, 0);
break;

case VK_DOWN:
yCaret = min(yCaret + 1, cyBuffer - 1);
break;

case VK_DELETE://退格键
for (x = xCaret; x < cxBuffer - 1; x++)//退格键的作用是当前行当前位置之后的所有字符前移一个字符的宽度,删除当前字符
BUFFER(x, yCaret) = BUFFER(x + 1, yCaret);

BUFFER(cxBuffer - 1, yCaret) = ' ';//退格前本行最后一个字符置空格

HideCaret(hwnd);//临时隐藏插入字符,必须的操作,否则插入字符后面会拖着个黑框
hdc = GetDC(hwnd);

SelectObject(hdc, CreateFont(0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL));
TextOut(hdc, xCaret * cxChar, yCaret * cyChar,
&BUFFER(xCaret, yCaret),
cxBuffer - xCaret);//重新打印该行
DeleteObject(SelectObject(hdc, GetStockObject
(SYSTEM_FONT)));//这句话写不写无所谓
ReleaseDC(hwnd, hdc);
ShowCaret(hwnd);
break;
}
SetCaretPos(xCaret * cxChar, yCaret * cyChar);//设置插入字符的位置
return 0;

字符消息

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
case WM_CHAR:
for (i = 0; i < (int)LOWORD(lParam); i++)
{
switch (wParam)
{
case '\b': // backspace
if (xCaret > 0)//退格最多退到本行开始
{
xCaret--;//插入符号的位置左移
SendMessage(hwnd, WM_KEYDOWN, VK_DELETE,
1);//发生退格的时候要删除字符
}
break;
case '\t': // tab
do
{
SendMessage(hwnd, WM_CHAR, ' ', 1);
} while (xCaret % 8 != 0);//右移直到xCaret位置8字符对齐
break;
case '\n': // line feed
if (++yCaret == cyBuffer)//回车后需要跳到下一行相同x位置
yCaret = 0;
break;

case '\r': // carriage return
xCaret = 0;

if (++yCaret == cyBuffer)
yCaret = 0;
break;

case '\x1B': // escape ,清空缓冲区
for (y = 0; y < cyBuffer; y++)
for (x = 0; x < cxBuffer; x++)
BUFFER(x, y) = ' ';//遍历清空缓冲区

xCaret = 0;
yCaret = 0;

InvalidateRect(hwnd, NULL, FALSE);//清空缓冲区后立刻重绘
break;

default: // character codes ,其他字符看作可打印字符,存入缓冲区
BUFFER(xCaret, yCaret) = (TCHAR)wParam;

HideCaret(hwnd);
hdc = GetDC(hwnd);

SelectObject(hdc, CreateFont(0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL));
TextOut(hdc, xCaret * cxChar, yCaret * cyChar,
&BUFFER(xCaret, yCaret), 1);
DeleteObject(
SelectObject(hdc, GetStockObject(SYSTEM_FONT)));
ReleaseDC(hwnd, hdc);
ShowCaret(hwnd);
if (++xCaret == cxBuffer)//本行到头了,需要换行
{
xCaret = 0;
if (++yCaret == cyBuffer)
yCaret = 0;
}
break;
}
}

SetCaretPos(xCaret * cxChar, yCaret * cyChar);
return 0;

绘图消息

1
2
3
4
5
6
7
8
9
10
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);

SelectObject(hdc, CreateFont(0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL));//等宽字体,FIXED_PITCH等宽间距
for (y = 0; y < cyBuffer; y++)//按行遍历,每次打印一整行
TextOut(hdc, 0, y * cyChar, &BUFFER(0, y), cxBuffer);
DeleteObject(SelectObject(hdc,GetStockObject(SYSTEM_FONT)));
EndPaint(hwnd, &ps);
return 0;

窗口销毁消息

1
2
3
4
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

COFF文件格式

COFF

common object file format

文件整体结构如图

image-20220811105858318

映像文件头

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;

} IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;

与PE文件有区别

在PE文件中映像文件头就是NT文件头,它是PE头的成员NT头的成员

而在COFF中上来就是镜像文件头

使用dumpbin观察main.obj文件的映像文件头

1
2
3
4
5
6
7
8
9
10
11
12
Dump of file main.obj

File Type: COFF OBJECT

FILE HEADER VALUES
8664 machine (x64)
E number of sections
62F46FCA time date stamp Thu Aug 11 10:56:10 2022
4E5 file pointer to symbol table
30 number of symbols
0 size of optional header
0 characteristics

x86_64上的程序

14个节

编译时间2022.8.11 10:56:10

符号表的文件偏移量4E5

符号数量30h

可选头没有,PE中才有,COFF中永远都是0

特征没有

节头表

节头表是一个结构体数组,每个元素都是一个IMAGE_SECTION_HEADER结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节名
union {//联合
DWORD PhysicalAddress;//本段在文件中的地址
DWORD VirtualSize;//本段在虚拟内存中的大小
} Misc;
DWORD VirtualAddress;//相对虚拟地址
DWORD SizeOfRawData;//该段在文件中的大小
DWORD PointerToRawData;//该段在文件中的位置
DWORD PointerToRelocations;//该段的重定位表在文件中的位置
DWORD PointerToLinenumbers;//该段的行号表在文件中的位置
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;//段属性,包括读写执行等性质
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

这个表有多少个元素呢?在映像文件头中的NumberOfSections指明了节头表的元素数量

.drectve

比如第一个节区头就是.drectve的节区头

Directive,编译器传递给链接器的指令,作用和在命令行上link直接指定命令行参数相同

只不过这样写可以给每个目标模块定制一套链接参数,link的时候不需要再写一长串命令行参数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SECTION HEADER #1
.drectve name
0 physical address
0 virtual address
18 size of raw data
244 file pointer to raw data (00000244 to 0000025B)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
100A00 flags
Info #0x200,该段包含的是注释或者其他信息
Remove #0x800,链接成exe文件时该段最终被抛弃
1 byte align #0x100000,1字节对齐,相当于没有对齐要求

.debug

调试信息

.debug$S表示包含符号相关的调试信息段

.debug$P表示预编译头的调试信息段

.debug$T表示类型相关的调试信息段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SECTION HEADER #2
.debug$S name
0 physical address
0 virtual address
74 size of raw data
11C file pointer to raw data (0000011C to 0000018F)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42100040 flags
Initialized Data
Discardable
1 byte align
Read Only

节区

节区紧跟在节头表后面,也就是正文部分

使用dumpbin观察节区数量

.drectve

第一个节区就是链接指示信息节区,节区表指示该节区正文位于文件的0x244位置

dumpbin打印的结果中,正文区是紧跟在节头后面的

dumpbin知道这个段是关于链接信息的段,它直接把ASCII码翻译出来了

1
2
3
4
5
6
7
RAW DATA #1
00000000: 20 20 20 2F 44 45 46 41 55 4C 54 4C 49 42 3A 22 /DEFAULTLIB:"
00000010: 4C 49 42 43 4D 54 22 20 LIBCMT"

Linker Directives
-----------------
/DEFAULTLIB:LIBCMT

就相当于在链接时使用

1
link main.obj /DEFAULTLIB:LIBCMT

这也就解释了为啥我们没有显示链接LIBCMT库但是编译器自动要求链接这个库,默认的链接信息已经写入main.obj的.drectve节了

.debug

1
2
3
4
5
6
7
8
9
RAW DATA #2
00000000: 04 00 00 00 F1 00 00 00 66 00 00 00 28 00 01 11 ....?..f...(...
00000010: 00 00 00 00 43 3A 5C 55 73 65 72 73 5C 38 36 31 ....C:\Users\861
00000020: 33 35 5C 64 65 73 6B 74 6F 70 5C 6C 69 6E 6B 5C 35\desktop\link\
00000030: 61 2E 6F 62 6A 00 3A 00 3C 11 00 62 00 00 D0 00 a.obj.:.<..b..?
00000040: 13 00 1F 00 82 79 02 00 13 00 1F 00 82 79 02 00 .....y.......y..
00000050: 4D 69 63 72 6F 73 6F 66 74 20 28 52 29 20 4F 70 Microsoft (R) Op
00000060: 74 69 6D 69 7A 69 6E 67 20 43 6F 6D 70 69 6C 65 timizing Compile
00000070: 72 00 00 00

符号表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

COFF SYMBOL TABLE
000 01047982 ABS notype Static | @comp.id
001 80010190 ABS notype Static | @feat.00
002 00000002 ABS notype Static | @vol.md
003 00000000 SECT1 notype Static | .drectve
Section length 18, #relocs 0, #linenums 0, checksum 0
005 00000000 SECT2 notype Static | .debug$S
Section length 74, #relocs 0, #linenums 0, checksum 0
007 00000000 SECT3 notype Static | .text$mn
Section length 24, #relocs 2, #linenums 0, checksum 9B262C10
009 00000000 SECT3 notype () External | main
00A 00000000 UNDEF notype () External | swap
00B 00000000 SECT3 notype Label | $LN3
00C 00000000 SECT4 notype Static | .xdata
Section length 8, #relocs 0, #linenums 0, checksum 37887F31
00E 00000000 SECT4 notype Static | $unwind$main
00F 00000000 SECT5 notype Static | .pdata
Section length C, #relocs 3, #linenums 0, checksum 7D3C6CAC
011 00000000 SECT5 notype Static | $pdata$main
012 00000000 UNDEF notype External | shared
013 00000000 SECT6 notype Static | .chks64
Section length 30, #relocs 0, #linenums 0, checksum 0

最左侧是符号的编号,也就是符号在符号表中的下标,

第二列是符号的大小,即符号所表示的对象占用的空间

第三列是符号所在段位置

ABS表示符号不属于任何段

SECT1表示符号属于COFF文件的第一个段,即.drectve段

UNDEF表示符号未定义,存在于其他文件,需要链接决议

第四列是符号类型

notype:变量和其他符号

notype():函数类型

第五列是访问能见度

Static为本模块可见

Externel为全局变量,可以被其他模块引用

第六列是符号名

如果符号是一个段名则dumpbin会紧接着在下一行列出段属性,包括段长度,段重定位数,行号数和检校和

工具

MSVC

cl /c 编译不链接

cl /Za 禁用Visual C++拓展

cl /Zl 关闭默认C库的链接

dumpbin

类似于GNU objdump

dumpbin /SUMMARY 打印段名段长

dump /ALL 打印全部信息

效果类似于010editor 的模板

chapter3-ELF精灵

可以先看CSAPP chapter 7链接然后看程序员自我修养这本书

历史与简介

一个程序的编译链接全过程

分类:

目标文件可以分为两种,可重定位目标文件,和可执行目标文件

其中可重定位目标模块在linux上是.o,在windows上是.obj.源代码只经过编译,不通过链接得到可重定位目标模块.其中的代码数据从0开始编址,只具有相对意义,无绝对意义

一个或者多个可重定位目标模块,与库文件链接后,形成可执行目标文件,在windows上是.exe,在linux上是.out

库文件,包括动态库和静态库,也是按照目标文件的结构存储的

COFF分类

image-20220805145133735

历史:

最初Unix上的可执行文件格式为COFF(common file format)

从Unix进化而来的Windows,Linux分别有不同的变种

Windows NT上是PE-COFF(portable executable)

Linux上是ELF(Executable Linkable Format)

Linux ELF格式

ELF
image-20220805153055752

linux源码中ELF格式定义在elf.h

1
2
3
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# whereis elf.h
elf.h: /usr/include/elf.h

关于该elf.h中的基本数据类型,没有直接使用诸如int,char等类型,而是使用typedef重新包装了一下

image-20220805154752930

就一个Half类型需要注意,不管是32位系统还是64位系统上该值都是2字节

ELF头

使用readelf -h main即可观察elf头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# readelf -h main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1040
Start of program headers: 64 (bytes into file)
Start of section headers: 14080 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29

64位elf头格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define EI_NIDENT (16)

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;

e_ident

整个ELF文件开头的十六个无符号字符类型,作用是表征文件魔数以及其他信息

7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00

前16个字节翻译成人话:4位 ELF version1 小端存储文件

image-20220805153933915

这16个字符分别代表的信息在elf.h中给出了宏定义

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

/* Fields in the e_ident array. The EI_* macros are indices into the
array. The macros under each EI_* macro are the values the byte
may have. */

#define EI_MAG0 0 /* File identification byte 0 index */
#define ELFMAG0 0x7f /* Magic number byte 0 */

#define EI_MAG1 1 /* File identification byte 1 index */
#define ELFMAG1 'E' /* Magic number byte 1 */

#define EI_MAG2 2 /* File identification byte 2 index */
#define ELFMAG2 'L' /* Magic number byte 2 */

#define EI_MAG3 3 /* File identification byte 3 index */
#define ELFMAG3 'F' /* Magic number byte 3 */

/* Conglomeration of the identification bytes, for easy testing as a word. */
#define ELFMAG "\177ELF"
#define SELFMAG 4

#define EI_CLASS 4 /* File class byte index */
#define ELFCLASSNONE 0 /* Invalid class */
#define ELFCLASS32 1 /* 32-bit objects */
#define ELFCLASS64 2 /* 64-bit objects */
#define ELFCLASSNUM 3
....
前四字符e_ident[EI_MAG0]~e_ident[EI_MAG3]
1
2
3
4
5
6
7
8
9
10
11
#define EI_MAG0		0		/* File identification byte 0 index */
#define ELFMAG0 0x7f /* Magic number byte 0 */

#define EI_MAG1 1 /* File identification byte 1 index */
#define ELFMAG1 'E' /* Magic number byte 1 */

#define EI_MAG2 2 /* File identification byte 2 index */
#define ELFMAG2 'L' /* Magic number byte 2 */

#define EI_MAG3 3 /* File identification byte 3 index */
#define ELFMAG3 'F' /* Magic number byte 3 */

前四个字符7F454C46的ASCII码是[DEL]ELF,ELF魔数前面有一个DEL删除符,不是ASCII可打印字符

1
2
3
/* Conglomeration of the identification bytes, for easy testing as a word.  */
#define ELFMAG "\177ELF"
#define SELFMAG 4
第五个字符e_ident[EI_CLASS]
1
2
3
4
#define EI_CLASS	4		/* File class byte index */
#define ELFCLASSNONE 0 /* Invalid class */
#define ELFCLASS32 1 /* 32-bit objects */
#define ELFCLASS64 2 /* 64-bit objects */

0表示无效elf文件

1表示32位elf文件

2表示64位elf文件

第六个字符e_ident[EI_DATA]
1
2
3
4
#define EI_DATA		5		/* Data encoding byte index */
#define ELFDATANONE 0 /* Invalid data encoding */
#define ELFDATA2LSB 1 /* 2's complement, little endian */
#define ELFDATA2MSB 2 /* 2's complement, big endian */

规定数据大小端顺序

0无效格式

1小端格式

2大端格式

第七个字符e_ident[EI_VERSION]
1
2
#define EI_VERSION	6		/* File version byte index */
/* Value must be EV_CURRENT */
1
2
3
4
/* Legal values for e_version (version).  */

#define EV_NONE 0 /* Invalid ELF version */
#define EV_CURRENT 1 /* Current version */

注释中写道,该值必须为EV_CURRENT(1)

其意义是ELF的主版本号,为啥一定是1呢?

因为ELF最新版本是1.2,之后没有更新,因此最新版本也就是先行版本,就是1

后面9个字符elf标准没有要求,一般置0,但是elf.h中是有意义的

第八个字符e_ident[EI_OSABI]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define EI_OSABI	7		/* OS ABI identification */
#define ELFOSABI_NONE 0 /* UNIX System V ABI */
#define ELFOSABI_SYSV 0 /* Alias. */
#define ELFOSABI_HPUX 1 /* HP-UX */
#define ELFOSABI_NETBSD 2 /* NetBSD. */
#define ELFOSABI_GNU 3 /* Object uses GNU ELF extensions. */
#define ELFOSABI_LINUX ELFOSABI_GNU /* Compatibility alias. */
#define ELFOSABI_SOLARIS 6 /* Sun Solaris. */
#define ELFOSABI_AIX 7 /* IBM AIX. */
#define ELFOSABI_IRIX 8 /* SGI Irix. */
#define ELFOSABI_FREEBSD 9 /* FreeBSD. */
#define ELFOSABI_TRU64 10 /* Compaq TRU64 UNIX. */
#define ELFOSABI_MODESTO 11 /* Novell Modesto. */
#define ELFOSABI_OPENBSD 12 /* OpenBSD. */
#define ELFOSABI_ARM_AEABI 64 /* ARM EABI */
#define ELFOSABI_ARM 97 /* ARM */
#define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */

OS ABI identification,操作系统 应用 二进制 接口 标识

关于ABI,参考了你们说的ABI,Application Binary Interface到底是什么东西? - 知乎 (zhihu.com)

关键点:

ABI实际上讨论的是什么?

那么当人们提到 ABI 的时候,到底在说什么?以我个人的经验来看,当人们提及 ABI 时,一般主要是在说 Binary-compatible 即二进制兼容性。

什么是二进制兼容性?

一个库在 VC9 上完成编译并以 DLL 形式发布,如果该库要求使用它的应用程序也必须在 VC9 上编译,那么说这个库不是二进制兼容的;反之,如果任意版本的 VC 乃至其它编译器例如 gcc、clang 都可以使用这个库,那么说这个库是二进制兼容的。

二进制兼容性包括调用约定,命名管理

ABI(Application Binary Interface):应用程序二进制接口,描述了应用程序和操作系统之间,一个应用和它的库之间,或者应用的组成部分之间的低接口。ABI涵盖了各种细节,如:

  • 数据类型的大小、布局和对齐;
  • 调用约定(控制着函数的参数如何传送以及如何接受返回值),例如,是所有的参数都通过栈传递,还是部分参数通过寄存器传递;哪个寄存器用于哪个函数参数;通过栈传递的第一个函数参数是最先push到栈上还是最后;
  • 系统调用的编码和一个应用如何向操作系统进行系统调用;
  • 以及在一个完整的操作系统ABI中,目标文件二进制格式、程序库等等。

ELFOSABI_NONE(0)表明本文件满足UNIX System V ABI规范,已成为主要的Unix操作系统(例如Linux,BSD系统和许多其他操作系统)使用的标准ABI

那么调用约定就得查UNIX System V,查stdcall,fastcall,cdecl等等没有没用

第九个字符e_ident[EI_ABIVERSION]

ABI版本号,然而实际上都是0

剩下七个字符

全是胸垫,目前没有作用

1
#define EI_PAD		9		/* Byte index of padding bytes */

e_type

目标文件类型,区分库文件,可重定位目标模块,可执行目标文件等等

1
2
3
4
5
6
7
/* Legal values for e_type (object file type).  */

#define ET_NONE 0 /* No file type */
#define ET_REL 1 /* Relocatable file 可重定位.o文件*/
#define ET_EXEC 2 /* Executable file 可执行.out文件*/
#define ET_DYN 3 /* Shared object file 动态库.so文件*/
#define ET_CORE 4 /* Core file 核心文件*/

e_machine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/* Legal values for e_machine (architecture). */

#define EM_NONE 0 /* No machine */
#define EM_M32 1 /* AT&T WE 32100 */
#define EM_SPARC 2 /* SUN SPARC */
#define EM_386 3 /* Intel 80386 */
#define EM_68K 4 /* Motorola m68k family */
#define EM_88K 5 /* Motorola m88k family */
#define EM_IAMCU 6 /* Intel MCU */
#define EM_860 7 /* Intel 80860 */
#define EM_MIPS 8 /* MIPS R3000 big-endian */
#define EM_S370 9 /* IBM System/370 */
#define EM_MIPS_RS3_LE 10 /* MIPS R3000 little-endian */
...
#define EM_X86_64 62 /* AMD x86-64 architecture */
...
#define EM_NUM 253

e_machine的有效值总共有253个,每一个代表一种机器类型

可执行目标文件main中该值为EM_X86_64(62),意思是本程序只能在x86_64体系上执行,

在x86_32上或者MIPS等等都不行

e_version

elf版本号,有效值只有1,也就是先行ELF文件版本,作用和e_ident[EI_VERSION]相同

1
2
3
4
/* Legal values for e_version (version).  */

#define EV_NONE 0 /* Invalid ELF version */
#define EV_CURRENT 1 /* Current version */

e_entry

ELF文件的入口点,如果没有入口点,比如库文件,则该值默认为0

main文件中该值为0x1040

image-20220805164912919

本文件开始运行后,操作系统将控制转移给该位置的指令

用ida64打开main按下g跳转0x1040,发现是start的RVA(虚拟内存相对偏移量)地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000001040                 public start
.text:0000000000001040 start proc near ; DATA XREF: LOAD:0000000000000018↑o
.text:0000000000001040 ; __unwind {
.text:0000000000001040 xor ebp, ebp
.text:0000000000001042 mov r9, rdx ; rtld_fini
.text:0000000000001045 pop rsi ; argc
.text:0000000000001046 mov rdx, rsp ; ubp_av
.text:0000000000001049 and rsp, 0FFFFFFFFFFFFFFF0h
.text:000000000000104D push rax
.text:000000000000104E push rsp ; stack_end
.text:000000000000104F lea r8, __libc_csu_fini ; fini
.text:0000000000001056 lea rcx, __libc_csu_init ; init
.text:000000000000105D lea rdi, main ; main
.text:0000000000001064 call cs:__libc_start_main_ptr
.text:000000000000106A hlt
.text:000000000000106A ; } // starts at 1040
.text:000000000000106A start endp

即装载器装载该文件进入内存之后,控制转移到进程映像相对映像基址的偏移量为0x1040的地方

该值可以在链接时修改,gcc -e指定入口函数

e_phoff

program headeer table offset

程序头部表在文件中的字节偏移量,如果没有程序头部表则该值为0

image-20220805170202544

main中该值为64D=40H

image-20220805170330203

而本文件的0x40位置确实是程序头部表的基地址

e_shoff

section header table offset

节头部表在文件中的字节偏移量,如果没有节头表则该值为0

image-20220805170506360

main中该值为14090D=3700h

image-20220805170723100

本文件的0x3700位置确实是节头表

e_flags

1
2
3
4
5
6
7
8
9
10
11
12
/* Values for Elf64_Ehdr.e_flags.  */

#define EF_SPARCV9_MM 3
#define EF_SPARCV9_TSO 0
#define EF_SPARCV9_PSO 1
#define EF_SPARCV9_RMO 2
#define EF_SPARC_LEDATA 0x800000 /* little endian data */
#define EF_SPARC_EXT_MASK 0xFFFF00
#define EF_SPARC_32PLUS 0x000100 /* generic V8+ features */
#define EF_SPARC_SUN_US1 0x000200 /* Sun UltraSPARC1 extensions */
#define EF_SPARC_HAL_R1 0x000400 /* HAL R1 extensions */
#define EF_SPARC_SUN_US3 0x000800 /* Sun UltraSPARCIII extensions */

处理器相关标志

e_ehsize

ELF文件头的大小

image-20220805171525758

main中该值为64D=40H

即前64个字节是ELF header,后面紧接着是程序头表(如果存在的话)

e_phentsize

程序头表中表项的大小(每个表项一样大)

image-20220805172137637

main中该值为56D

e_phnum

程序头项数

image-20220805174430352

main中该值为23D

程序头项数乘以每项大小就可以计算得到程序头总大小56*23=1288字节

e_shentsize

section header entry size

节头表表项大小

image-20220805174753025

main中该值为64D=40h

e_shnum

节头表项数

e_shstrndx

section header string table index,节名字符串表(.shstrtab)的表头在节头表中的下标

image-20220806102105072

记录这么一个东西的目的是,方便获取各个节的名称,ELF头大小固定,节头表的基址在ELF头中已经给出,e_shoff,节头表表项大小业已给出e_shentsize,那么e_shoff+e_shentsize*e_shstrndx就索引到了节名字符串表,方便获取节名

程序头表

Program Header Table ,是Elf64_Phdr结构体数组,描述段信息或者准备程序执行所需要的信息

一个段可能包含多个节.

程序运行时才会把多个性质相同的节合并成段,因此段表只对可执行目标文件有意义

可重定位目标文件中没有程序头表

image-20220806081713545

main的程序头表中有13项,下标从0到12

程序头表项结构体struct Elf64_Phdr(program header descriptor)

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;

以栈段为例子,炎鸠一下段表项各成员的含义

image-20220806082000923

p_type

段类型,枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Legal values for p_type (segment type).  */

#define PT_NULL 0 /* Program header table entry unused */
#define PT_LOAD 1 /* Loadable program segment */
#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Program interpreter */
#define PT_NOTE 4 /* Auxiliary information */
#define PT_SHLIB 5 /* Reserved */
#define PT_PHDR 6 /* Entry for header table itself */
#define PT_TLS 7 /* Thread-local storage segment */
#define PT_NUM 8 /* Number of defined types */
#define PT_LOOS 0x60000000 /* Start of OS-specific */
#define PT_GNU_EH_FRAME 0x6474e550 /* GCC .eh_frame_hdr segment */
#define PT_GNU_STACK 0x6474e551 /* Indicates stack executability */
#define PT_GNU_RELRO 0x6474e552 /* Read-only after relocation */
#define PT_GNU_PROPERTY 0x6474e553 /* GNU property */
#define PT_LOSUNW 0x6ffffffa
#define PT_SUNWBSS 0x6ffffffa /* Sun Specific segment */
#define PT_SUNWSTACK 0x6ffffffb /* Stack segment */
#define PT_HISUNW 0x6fffffff
#define PT_HIOS 0x6fffffff /* End of OS-specific */
#define PT_LOPROC 0x70000000 /* Start of processor-specific */
#define PT_HIPROC 0x7fffffff /* End of processor-specific */

PT_NULL(0)表明该段未使用,对应程序头表项其他成员均没有定义

p_flags

1
2
3
4
5
6
7
/* Legal values for p_flags (segment flags).  */

#define PF_X (1 << 0) /* Segment is executable */
#define PF_W (1 << 1) /* Segment is writable */
#define PF_R (1 << 2) /* Segment is readable */
#define PF_MASKOS 0x0ff00000 /* OS-specific */
#define PF_MASKPROC 0xf0000000 /* Processor-specific */

段权限,主要是RWX属性

比如使用最简单的gcc命令编译成的main栈段的权限:

image-20220806083553366

此时栈是没有执行权限

如果编译时禁用NX保护gcc -z execstack main.c -o main,此时栈就可以读写执行了

image-20220806084047470

默认情况下是开启NX保护的

gcc -z execstack 关闭NX保护

gcc -z noexecstack开启NX保护

p_offset

文件偏移量,如果该段是程序运行时才会建立,比如堆栈,则该值为0

p_vaddr

相对虚拟地址

p_paddr

物理地址,该值只在直接使用物理内存的机器和系统上使用

p_filesz

文件中该段的大小

p_memsz

内存镜像中该段的大小

p_align

该段的对齐要求

该值为0或者1表示不用对齐,否则按照2的幂次对齐

要求p_vaddrp_offsetp_align要相等

节头表

main中节头表有30项

每一项都是Elf64_Shdr结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;

sh_name

这里name不是直接的ASCII码表示的字符串,而是.shstrtab这个节中的偏移

一个Elf64_Worduint_32,就四个字节,但是节名显然可以更长,比如.got.plt

比如.data节节名的下标就是FCh

image-20220806085921448

到shstrtab的节头看看

image-20220806090138926

发现该节在文件中的偏移量为35ED,到该节中看看

image-20220806090231225

该位置正是所有节名字符串的起点,所有的节名都是一块存放的,均以NULL结尾.那么划分节名的时候就是从本节区初,预见NULL则划分一个字符串作为一个节名

可以发现节区初0x35ED就有一个NULL,这对应一个无名的节区(SHN_UNDEF)

image-20220806090713731

35ED+FC=36E9

image-20220806090419728

这个位置正是".data"字符串的起始位置

sh_type

节类型

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
/* Legal values for sh_type (section type).  */

#define SHT_NULL 0 /* Section header table entry unused */
#define SHT_PROGBITS 1 /* Program data */
#define SHT_SYMTAB 2 /* Symbol table */
#define SHT_STRTAB 3 /* String table */
#define SHT_RELA 4 /* Relocation entries with addends */
#define SHT_HASH 5 /* Symbol hash table */
#define SHT_DYNAMIC 6 /* Dynamic linking information */
#define SHT_NOTE 7 /* Notes */
#define SHT_NOBITS 8 /* Program space with no data (bss) */
#define SHT_REL 9 /* Relocation entries, no addends */
#define SHT_SHLIB 10 /* Reserved */
#define SHT_DYNSYM 11 /* Dynamic linker symbol table */
#define SHT_INIT_ARRAY 14 /* Array of constructors */
#define SHT_FINI_ARRAY 15 /* Array of destructors */
#define SHT_PREINIT_ARRAY 16 /* Array of pre-constructors */
#define SHT_GROUP 17 /* Section group */
#define SHT_SYMTAB_SHNDX 18 /* Extended section indices */
#define SHT_NUM 19 /* Number of defined types. */
#define SHT_LOOS 0x60000000 /* Start OS-specific. */
#define SHT_GNU_ATTRIBUTES 0x6ffffff5 /* Object attributes. */
#define SHT_GNU_HASH 0x6ffffff6 /* GNU-style hash table. */
#define SHT_GNU_LIBLIST 0x6ffffff7 /* Prelink library list */
#define SHT_CHECKSUM 0x6ffffff8 /* Checksum for DSO content. */
#define SHT_LOSUNW 0x6ffffffa /* Sun-specific low bound. */
#define SHT_SUNW_move 0x6ffffffa
#define SHT_SUNW_COMDAT 0x6ffffffb
#define SHT_SUNW_syminfo 0x6ffffffc
#define SHT_GNU_verdef 0x6ffffffd /* Version definition section. */
#define SHT_GNU_verneed 0x6ffffffe /* Version needs section. */
#define SHT_GNU_versym 0x6fffffff /* Version symbol table. */
#define SHT_HISUNW 0x6fffffff /* Sun-specific high bound. */
#define SHT_HIOS 0x6fffffff /* End OS-specific type */
#define SHT_LOPROC 0x70000000 /* Start of processor-specific */
#define SHT_HIPROC 0x7fffffff /* End of processor-specific */
#define SHT_LOUSER 0x80000000 /* Start of application-specific */
#define SHT_HIUSER 0x8fffffff /* End of application-specific */
image-20220806090940795

data节的类型为SHT_PROGBITS(1),意思是程序数据

bss节的类型是SHT_NOBITS(8),专门给bss节设置的节类型

sh_flags

节标志位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Legal values for sh_flags (section flags).  */

#define SHF_WRITE (1 << 0) /* Writable */
#define SHF_ALLOC (1 << 1) /* Occupies memory during execution */
#define SHF_EXECINSTR (1 << 2) /* Executable */
#define SHF_MERGE (1 << 4) /* Might be merged */
#define SHF_STRINGS (1 << 5) /* Contains nul-terminated strings */
#define SHF_INFO_LINK (1 << 6) /* `sh_info' contains SHT index */
#define SHF_LINK_ORDER (1 << 7) /* Preserve order after combining */
#define SHF_OS_NONCONFORMING (1 << 8) /* Non-standard OS specific handling
required */
#define SHF_GROUP (1 << 9) /* Section is member of a group. */
#define SHF_TLS (1 << 10) /* Section hold thread-local data. */
#define SHF_COMPRESSED (1 << 11) /* Section with compressed data. */
#define SHF_MASKOS 0x0ff00000 /* OS-specific. */
#define SHF_MASKPROC 0xf0000000 /* Processor-specific */
#define SHF_GNU_RETAIN (1 << 21) /* Not to be GCed by linker. */
#define SHF_ORDERED (1 << 30) /* Special ordering requirement
(Solaris). */
#define SHF_EXCLUDE (1U << 31) /* Section is excluded unless
referenced or allocated (Solaris).*/

SHF_WRITE:该节在进程虚拟空间中可写

SHF_ALLOC:该节在进程虚拟内存中需要分配空间,比如text,data,bss节

SHF_EXECINSTR:该节在进程虚拟空间中可执行,比如text

都是2的幂次,多个属性按位或

.data节的标志是3,意味着可以分配,可以执行

sh_addr

节虚拟地址,如果该节会被加载,则该值为节在虚拟内存中的地址

sh_offset

节文件偏移,不是本节头的文件偏移,而是本节头指向对应的节的偏移量

为啥要节和节头分家?

每个节的大小固定,如果有额外信息,比如shrstrtab需要额外保存字符数组,则用该成员作为指针另外指向一片区域

这样加载时读取节头不用担心有大有小,直接读取整个节头表即可

sh_size

节区大小,不是节头大小,而是节头指向的对应节区的大小

sh_link&sh_info

节链接信息,这两个成员的意义视sh_type而定

来自CTF wiki:

sh_type sh_link sh_info
SHT_DYNAMIC 节区中使用的字符串表的节头索引 0
SHT_HASH 此哈希表所使用的符号表的节头索引 0
SHT_REL/SHT_RELA 与符号表相关的节头索引 重定位应用到的节的节头索引
SHT_SYMTAB/SHT_DYNSYM 操作系统特定信息,Linux 中的 ELF 文件中该项指向符号表中符号所对应的字符串节区在 Section Header Table 中的偏移。 操作系统特定信息
other SHN_UNDEF 0

sh_addralign

节虚拟地址对齐要求

sh_entsize

如果本节也是一个数组,则本项目表征的是数组元素大小

比如got表

image-20220806092014449

其表项就是一个动态库中的实际虚拟地址,在64位系统上,8字节即可寻址整个虚拟地址空间,因此got节头中该值为8

节区

程序员的自我修养P77给了一张SimpleSection.o的文件视图

image-20220806094931783

越看越不对劲,最后发现不对劲是因为,.o文件不需要程序头表也就罢了,节头表section Table为啥没有紧挨着ELF头?而是夹在一些节区中间?

ELF头后面紧跟着的是text节

推测,Section Table后面的是链接需要用到的节,前面链接用不到

image-20220806095428497

010editor将符号表和动态符号表与节区头表,程序头表,elf头并列分析了

而实际上symbol_table和dynamic_symbol_table都是节区,只不过这两个节区都是表

字符串表

对应节头是这样的:

image-20220806100917610

s_offset表明字符串表的文件偏移为0x33F0

s_size表明字符串表的大小是509字节

那么字符串表在文件中的位置就是[33F0h,35F9h]

image-20220806101143997

其中的字符串均以NULL结尾,那么需要引用其中的字符串时,只需要给出相应字符串在字符串表中的下标或者偏移量,不需要再给出长度或者结尾的偏移

找到NULL就意味着字符串结束

节名字符串表

作用类似于字符串表,都是字符数组,只不过节名字符串表专门存放节名字符串

本节对应节头是这样写的:

image-20220806101528306

最下面这个char data[272]是010editor故意把节直接放到这里观察了,就是节名字符串表

image-20220806101712880

各个节头中的Elf32_Shdr.sh_name成员,就是本表中的字符串偏移量

符号表

符号:函数和变量统称符号

为了更清晰地观察符号表写一个test.c并编译成可重定位目标模块

test.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int global_initialized=10;
int global_uninitialized;
static int static_initialized=20;
static int static_uninitialized;
int main(){
int local;
printf("%d,%d,%d,%d,%d",global_initialized,global_uninitialized,static_initialized,static_uninitialized,local);
return 0;
}
1
gcc test.c -O0 -c -o test.o

符号表对应的节头是这样写的:

image-20220806103614207

s_offset表明符号表的基地址在文件中的F8h

image-20220806103655607

确实是一个表,有12项,其中就包括static_initialized等全局或者静态变量,main,printf等函数名

每个表项的结构相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;

注意64位和32位的结构体成员顺序不一样

st_name

本符号名字符串在strtab字符串表中的偏移量

比如static_initialized这一项中,该值为8h

image-20220806103940309

去查strtab表,对应strtab表头的sh_offset表明strtab表在0x218,那么static_initialized字符串就应该是从0x220开始一直到NULL结尾

image-20220806104112403

确实如此

st_info

符号类型和绑定信息

st_info为unsigned char类型,8位,高四位和低四位分别表示符号绑定信息和符号类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//低四位表示符号类型
/* Legal values for ST_BIND subfield of st_info (symbol binding). */

#define STB_LOCAL 0 /* Local symbol,局部符号 */
#define STB_GLOBAL 1 /* Global symbol,全局符号 */
#define STB_WEAK 2 /* Weak symbol,弱引用 */
#define STB_NUM 3 /* Number of defined types. 一共就三种符号,0,1,2*/
//STB_NUM表明一共就三种符号

//高四位表示符号绑定信息
/* Legal values for ST_TYPE subfield of st_info (symbol type). */

#define STT_NOTYPE 0 /* Symbol type is unspecified ,未知类型符号*/
#define STT_OBJECT 1 /* Symbol is a data object ,数据对象,数组,变量,对象*/
#define STT_FUNC 2 /* Symbol is a code object ,函数*/
#define STT_SECTION 3 /* Symbol associated with a section ,节*/
#define STT_FILE 4 /* Symbol's name is file name ,文件名*/
#define STT_COMMON 5 /* Symbol is a common data object */
#define STT_TLS 6 /* Symbol is thread-local data object*/
#define STT_NUM 7 /* Number of defined types. */
//STT_NUM表明一共就7种符号绑定信息

比如global_initialized这个符号,是一个全局的数据对象

image-20220806105817155

又比如test.c这个符号

image-20220806105930937

是一个局部文件名符号

st_other

预留的成员,目前没用

st_shndx

符号所在节的下标

比如main这个符号,不用想啃腚是在text节

image-20220806104544566

该值为1,去查节头表

image-20220806104627621

下标为1的节就是.text

对于不在本文件中定义的符号或者一些特殊符号,该值有特殊意义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Special section indices.  */

#define SHN_UNDEF 0 /* Undefined section ,本符号未在本文件定义,只在本文件中被引用,可能定义在其他文件中*/
#define SHN_LORESERVE 0xff00 /* Start of reserved indices */
#define SHN_LOPROC 0xff00 /* Start of processor-specific */
#define SHN_BEFORE 0xff00 /* Order section before all others
(Solaris). */
#define SHN_AFTER 0xff01 /* Order section after all others
(Solaris). */
#define SHN_HIPROC 0xff1f /* End of processor-specific */
#define SHN_LOOS 0xff20 /* Start of OS-specific */
#define SHN_HIOS 0xff3f /* End of OS-specific */
#define SHN_ABS 0xfff1 /* Associated symbol is absolute ,一个绝对的值*/
#define SHN_COMMON 0xfff2 /* Associated symbol is common ,该符号是一个COMMON伪节中的符号,未初始化的全局符号*/
#define SHN_XINDEX 0xffff /* Index is in extra table. */
#define SHN_HIRESERVE 0xffff /* End of reserved indices */

比如test.c这个符号

image-20220806110254225

该值为65521D=FFF1H,表示该符号包含了一个绝对的值

st_value

符号对应值,不同类型的文件中该值意义不同

在可执行目标文件中

该值表示符号的虚拟地址

比如main这个符号,其st_value值为0x1129

image-20220806110947749

用ida64加载观察之

image-20220806111114579

main确实在1129位置

在可重定位目标模块中

如果该符号是一个定义(不是引用,即st_shndx的值不是SHN_UNDEF),并且不是未初始化的全局变量(即不在COMMON伪节,即st_shndx不为SHN_COMMON),则该值表示的是该符号在对应节中的偏移量,这个节就是st_shndx指定的节

比如静态未初始化变量,

image-20220806111635430

st_shndx=4表明他在bss段

image-20220806111719286

st_value=0x4表明它的偏移量为4,即bss段的第五个字节,那么前四个字节是谁?global_uninitialized

image-20220806111854194

如果该符号在COMMON块,则st_value表示的是该符号的对齐要求

st_size

该符号表的大小

重定位表

重定位表可以有多个,如果text节需要重定位,则会有一个.rel.text,如果data节需要重定位,则会有一个.rel.data

test.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
extern int value;//外部变量
int add(int,int);//外部函数

int main(){
printf("test Link\n");
printf("sum=%d\n",add(5,6));
printf("value=%d",value);
return 0;
}
1
gcc test.c -c

对于只编译不链接得到的test.o,使用objdump反汇编观察其main函数

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
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# objdump -d test.o

test.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # b <main+0xb>
b: 48 89 c7 mov %rax,%rdi
e: e8 00 00 00 00 call 13 <main+0x13>
13: be 06 00 00 00 mov $0x6,%esi
18: bf 05 00 00 00 mov $0x5,%edi
1d: e8 00 00 00 00 call 22 <main+0x22>
22: 89 c6 mov %eax,%esi
24: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 2b <main+0x2b>
2b: 48 89 c7 mov %rax,%rdi
2e: b8 00 00 00 00 mov $0x0,%eax
33: e8 00 00 00 00 call 38 <main+0x38>
38: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3e <main+0x3e>
3e: 89 c6 mov %eax,%esi
40: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 47 <main+0x47>
47: 48 89 c7 mov %rax,%rdi
4a: b8 00 00 00 00 mov $0x0,%eax
4f: e8 00 00 00 00 call 54 <main+0x54>
54: b8 00 00 00 00 mov $0x0,%eax
59: 5d pop %rbp
5a: c3 ret

可以发现其中main+4,main+e,main+1d等等处的指令中有四个字节的0

实际上是由于,编译器对于__main函数,add函数,value外部变量无法解析其位置,于是全都置零等待链接器填坑

观察其重定位表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# objdump -r test.o

test.o: file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000007 R_X86_64_PC32 .rodata-0x0000000000000004
000000000000000f R_X86_64_PLT32 puts-0x0000000000000004 ;此处的puts是编译器将printf优化成了puts
000000000000001e R_X86_64_PLT32 add-0x0000000000000004
0000000000000027 R_X86_64_PC32 .rodata+0x0000000000000006
0000000000000034 R_X86_64_PLT32 printf-0x0000000000000004
000000000000003a R_X86_64_PC32 value-0x0000000000000004
0000000000000043 R_X86_64_PC32 .rodata+0x000000000000000e
0000000000000050 R_X86_64_PLT32 printf-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text

RELOCATION RECORDS FOR [.text]:这表示紧跟在后面是text节的符号的重定位条目

栏目 意义
OFFSET 该符号在其对应段中的偏移量
TYPE 重定位方式,这里有两种,PLT或者PC,是最常见的两种,PC重定位适用于变量的引用,PLT重定位适用于函数的重定位
VALUE 需要重定位的符号

比如0000000000000034 R_X86_64_PLT32 printf-0x0000000000000004这一条,

1
2
33:   e8 00 00 00 00          call   38 <main+0x38>
38: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3e <main+0x3e>

它恰好是main+33处的call的操作数(0xe8为call指令的操作码,后面的00就是main+34)

编译器会给每个需要重定位的符号建立一个重定位条目(然而自我修养上作者将Relocation Entry翻译成了重定位入口,感觉此处的Entry不如翻译成条目)

重定位条目是一个结构体:

1
2
3
4
5
6
7
/* The following, at least, is used on Sparc v9, MIPS, and Alpha.  */

typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
} Elf64_Rel;
r_offset

需要修正的位置,相对于该符号所在节的偏移量

就是objdump -r之后的OFFSET栏目

r_info

重定位条目的类型和符号

工具

Linux ELF

gcc编译链接

只编译不链接
1
gcc -c main.c -o main.o
编译链接
1
gcc main.c -o main

file查看文件格式

lilnux上file的参数可以为任何文件,其作用是报告该文件的基本信息

1
2
3
4
5
6
7
8
9
10
11
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# file main.c
main.c: C source, ASCII text, with CRLF line terminators

┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# file main.exe
main.exe: PE32+ executable (console) x86-64, for MS Windows

┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# file main
main: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=45f82af9109c5336fc17e25b88c4ce4def86b6e2, for GNU/Linux 3.2.0, not stripped

objdump 观察结构

objdump -h观察节区基本信息
栏目 意义
Idx 节区在节区表中的顺序编号
Name 节区名
Size 本节区大小
VMA
LMA
File off 本节区在文件中的偏移量
Align 本节区的对齐要求,2**n意思是\(2^n\)字节对齐,节区首地址必须是\(2^n\)的倍数
节区属性 CONTENTS
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
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# objdump -h main

main: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.gnu.property 00000020 0000000000000338 0000000000000338 00000338 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000000358 0000000000000358 00000358 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .note.ABI-tag 00000020 000000000000037c 000000000000037c 0000037c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gnu.hash 00000024 00000000000003a0 00000000000003a0 000003a0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynsym 00000090 00000000000003c8 00000000000003c8 000003c8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .dynstr 0000007d 0000000000000458 0000000000000458 00000458 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version 0000000c 00000000000004d6 00000000000004d6 000004d6 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .gnu.version_r 00000020 00000000000004e8 00000000000004e8 000004e8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.dyn 000000c0 0000000000000508 0000000000000508 00000508 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000017 0000000000001000 0000000000001000 00001000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000010 0000000000001020 0000000000001020 00001020 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt.got 00000008 0000000000001030 0000000000001030 00001030 2**3
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .text 00000161 0000000000001040 0000000000001040 00001040 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .fini 00000009 00000000000011a4 00000000000011a4 000011a4 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .rodata 00000004 0000000000002000 0000000000002000 00002000 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame_hdr 0000003c 0000000000002004 0000000000002004 00002004 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .eh_frame 00000108 0000000000002040 0000000000002040 00002040 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .init_array 00000008 0000000000003e18 0000000000003e18 00002e18 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .fini_array 00000008 0000000000003e20 0000000000003e20 00002e20 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .dynamic 000001b0 0000000000003e28 0000000000003e28 00002e28 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000028 0000000000003fd8 0000000000003fd8 00002fd8 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .got.plt 00000018 0000000000004000 0000000000004000 00003000 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .data 00000010 0000000000004018 0000000000004018 00003018 2**3
CONTENTS, ALLOC, LOAD, DATA
24 .bss 00000008 0000000000004028 0000000000004028 00003028 2**0
ALLOC
25 .comment 0000001e 0000000000000000 0000000000000000 00003028 2**0
CONTENTS, READONLY

各段作用

image-20220805152931491
objdump -s 以16进制打印各节内容
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
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# objdump -s main

main: file format elf64-x86-64

...
Contents of section .init:
1000 4883ec08 488b05dd 2f000048 85c07402 H...H.../..H..t.
1010 ffd04883 c408c3 ..H....
Contents of section .plt:
1020 ff35e22f 0000ff25 e42f0000 0f1f4000 .5./...%./....@.
Contents of section .plt.got:
1030 ff25c22f 00006690 .%./..f.
Contents of section .text:
1040 31ed4989 d15e4889 e24883e4 f050544c 1.I..^H..H...PTL
1050 8d054a01 0000488d 0de30000 00488d3d ..J...H......H.=
1060 c5000000 ff15762f 0000f40f 1f440000 ......v/.....D..
1070 488d3db1 2f000048 8d05aa2f 00004839 H.=./..H.../..H9
1080 f8741548 8b054e2f 00004885 c07409ff .t.H..N/..H..t..
1090 e00f1f80 00000000 c30f1f80 00000000 ................
10a0 488d3d81 2f000048 8d357a2f 00004829 H.=./..H.5z/..H)
10b0 fe4889f0 48c1ee3f 48c1f803 4801c648 .H..H..?H...H..H
10c0 d1fe7414 488b0525 2f000048 85c07408 ..t.H..%/..H..t.
10d0 ffe0660f 1f440000 c30f1f80 00000000 ..f..D..........
10e0 f30f1efa 803d3d2f 00000075 2b554883 .....==/...u+UH.
10f0 3d022f00 00004889 e5740c48 8b3d1e2f =./...H..t.H.=./
1100 0000e829 ffffffe8 64ffffff c605152f ...)....d....../
1110 0000015d c30f1f00 c30f1f80 00000000 ...]............
1120 f30f1efa e977ffff ff554889 e5b80000 .....w...UH.....
1130 00005dc3 662e0f1f 84000000 00006690 ..].f.........f.
1140 41574c8d 3dcf2c00 00415649 89d64155 AWL.=.,..AVI..AU
1150 4989f541 544189fc 55488d2d c02c0000 I..ATA..UH.-.,..
1160 534c29fd 4883ec08 e893feff ff48c1fd SL).H........H..
1170 03741b31 db0f1f00 4c89f24c 89ee4489 .t.1....L..L..D.
1180 e741ff14 df4883c3 014839dd 75ea4883 .A...H...H9.u.H.
1190 c4085b5d 415c415d 415e415f c30f1f00 ..[]A\A]A^A_....
11a0 c3 .
Contents of section .fini:
...
objdump -d 反汇编包含指令的段
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
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# objdump -d main

main: file format elf64-x86-64


Disassembly of section .init:

0000000000001000 <_init>:
1000: 48 83 ec 08 sub $0x8,%rsp
1004: 48 8b 05 dd 2f 00 00 mov 0x2fdd(%rip),%rax # 3fe8 <__gmon_start__@Base>
100b: 48 85 c0 test %rax,%rax
100e: 74 02 je 1012 <_init+0x12>
1010: ff d0 call *%rax
1012: 48 83 c4 08 add $0x8,%rsp
1016: c3 ret
....
0000000000001129 <main>:
1129: 55 push %rbp
112a: 48 89 e5 mov %rsp,%rbp
112d: b8 00 00 00 00 mov $0x0,%eax
1132: 5d pop %rbp
1133: c3 ret
1134: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
113b: 00 00 00
113e: 66 90 xchg %ax,%ax

...
objdump -r 观察重定位表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# objdump -r test.o

test.o: file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000007 R_X86_64_PC32 .rodata-0x0000000000000004
000000000000000f R_X86_64_PLT32 puts-0x0000000000000004
000000000000001e R_X86_64_PLT32 add-0x0000000000000004
0000000000000027 R_X86_64_PC32 .rodata+0x0000000000000006
0000000000000034 R_X86_64_PLT32 printf-0x0000000000000004
000000000000003a R_X86_64_PC32 value-0x0000000000000004
0000000000000043 R_X86_64_PC32 .rodata+0x000000000000000e
0000000000000050 R_X86_64_PLT32 printf-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text

size观察代码数据段的长度

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# size main
text data bss dec hex filename
1406 528 8 1942 796 main
;text代码段长度(10进制) data数据段长度(10进制) bss未初始化数据段(10进制) dec这三个段长度和(10进制) hex这三个段的长度和(16进制)

readelf观察ELF格式

readelf -h观察ELF头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# readelf -h main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1040
Start of program headers: 64 (bytes into file)
Start of section headers: 14080 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
readelf -S观察各节区
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
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# readelf -S main
There are 30 section headers, starting at offset 0x3700:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000358 00000358
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0
0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003c8 000003c8
0000000000000090 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000458 00000458
000000000000007d 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000004d6 000004d6
000000000000000c 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000000004e8 000004e8
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000508 00000508
00000000000000c0 0000000000000018 A 6 0 8
[11] .init PROGBITS 0000000000001000 00001000
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000001020 00001020
0000000000000010 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000001030 00001030
0000000000000008 0000000000000008 AX 0 0 8
[14] .text PROGBITS 0000000000001040 00001040
0000000000000161 0000000000000000 AX 0 0 16
[15] .fini PROGBITS 00000000000011a4 000011a4
0000000000000009 0000000000000000 AX 0 0 4
[16] .rodata PROGBITS 0000000000002000 00002000
0000000000000004 0000000000000004 AM 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000002004 00002004
000000000000003c 0000000000000000 A 0 0 4
[18] .eh_frame PROGBITS 0000000000002040 00002040
0000000000000108 0000000000000000 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000003e18 00002e18
0000000000000008 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000003e20 00002e20
0000000000000008 0000000000000008 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000003e28 00002e28
00000000000001b0 0000000000000010 WA 7 0 8
[22] .got PROGBITS 0000000000003fd8 00002fd8
0000000000000028 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000004000 00003000
0000000000000018 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000004018 00003018
0000000000000010 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000004028 00003028
0000000000000008 0000000000000000 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 00003028
000000000000001e 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00003048
00000000000003a8 0000000000000018 28 21 8
[28] .strtab STRTAB 0000000000000000 000033f0
00000000000001fd 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 000035ed
0000000000000110 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)

Nr:下标

Name:节名字符串在.shstrtab字符串表中的偏移量

Type:节类型

Address:节虚拟地址

Offset:节文件偏移

Size:节大小

EntSize:节项目(如果节是一个表的话)大小

Flags:节属性

Link&Info:节链接信息

Align:节对齐要求

readelf -s观察符号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# readelf -s test.o

Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 .data
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 .bss
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_initialized
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 static_uninitialized
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_initialized
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_uninitialized
10: 0000000000000000 70 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
栏目 意义
Num 符号表数组中的下标
Value st_value符号值
Size st_size符号大小
Type st_info低四位符号类型
Bind st_info高四位符号绑定信息
Vis st_other尚未使用,都是DEFAULT
Ndx st_shndx
Name 符号名称

nm观察符号表

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# nm -a test.o
0000000000000000 b .bss
0000000000000000 d .data
0000000000000000 D global_initialized
0000000000000000 B global_uninitialized
0000000000000000 T main
U printf
0000000000000000 r .rodata
0000000000000004 d static_initialized
0000000000000004 b static_uninitialized
0000000000000000 a test.c
0000000000000000 t .text

ar处理静态库

ar -t查看归档文件包括的目标模块
1
2
3
4
5
6
7
8
9
10
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/COFF]
└─# ar -t /usr/lib32/libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
...
ar -x将归档文件解包,释放所有.o目标模块