首页
社区
课程
招聘
[原创]和我一起学 --第一个GUI程序解释超详细
发表于: 2011-3-26 16:03 7595

[原创]和我一起学 --第一个GUI程序解释超详细

2011-3-26 16:03
7595
注释的很详细 孙鑫老师书里的代码
可以用来复习用吧
环境  VC6.0 打完防卡死补丁
MSDN 2001 VC6.0的最后一个版本的MSDN
VC6.0 SP6 以及补丁还有MSDN 我都传到网盘里去了 下载地址
http://dl.dbank.com/c0socichfc   英文补丁
http://dl.dbank.com/c0ax4bffyo vc6  1
http://dl.dbank.com/c0qno32v8e  vc6 2
http://dl.dbank.com/c0un8inukx  vc6 总卷
http://dl.dbank.com/c0r45ncbad  1
http://dl.dbank.com/c0klgrd5yj  6
http://dl.dbank.com/c09vcenzlq   5
http://dl.dbank.com/c0rvbw4kap  4
http://dl.dbank.com/c0t4k9ad90  3
http://dl.dbank.com/c0kywo11v6 2
http://dl.dbank.com/c07oq9tc5n  7
http://dl.dbank.com/c0tfm0vcuc 10
http://dl.dbank.com/c08kic6tp7  8
http://dl.dbank.com/c018by6py7 9
http://dl.dbank.com/c0syqhu7tf  11
http://dl.dbank.com/c0c6su4t06  z
http://dl.dbank.com/c0w08dqfzt 12
http://dl.dbank.com/c0o876tmzd MSDN总卷



原代码

#include <windows.h>
#include <stdio.h>

LRESULT CALLBACK WinSunProc(
  HWND hwnd,      // handle to window
  UINT uMsg,      // message identifier
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);

int WINAPI WinMain(
  HINSTANCE hInstance,      // handle to current instance
  HINSTANCE hPrevInstance,  // handle to previous instance
  LPSTR lpCmdLine,          // command line
  int nCmdShow              // show state
)
{
        WNDCLASS wndcls;  //wndclass一共有10个参数
        wndcls.cbClsExtra=0;//暂时不用赋值0 用到再说
        wndcls.cbWndExtra=0;//同上
        wndcls.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);//指定背景 由于返回的是HGDIOBJ要用HBRUSH进行强制转换
        wndcls.hCursor=LoadCursor(NULL,IDC_ARROW);//鼠标指针形状 可以填NULL 默认的指针 也可以用IDC_WAIT等 使用LoadCursor函数加载
        wndcls.hIcon=LoadIcon(NULL,IDI_ERROR);//左上角程序的标志可以是NULL 也可以用LoadIcon加载IDI_WARNING等等其他的标志
        wndcls.hInstance=hInstance;//可以是NULL 也可以直接赋值hInstance 但是如果有多个窗口一定要指明是哪个实例句柄
        wndcls.lpfnWndProc=WinSunProc;//传递回调函数的名称即可 在这里我们传递WinSunProc
        wndcls.lpszClassName="sunxin2006";// 窗口类的名字 不是在程序左上角标题的名字哦
        wndcls.lpszMenuName=NULL;//菜单的名字 我们没有使用菜单为NULL
        wndcls.style=CS_HREDRAW | CS_VREDRAW; //还有CS_NOCLOSE等 可用|设置多个参数
        //当水平和竖直方向的大小发生改变的时候将重绘 会阐释WM_PAINT消息 注意下面CASE语句中这个消息的处理
        RegisterClass(&wndcls);//注册窗口 创建窗口 展示窗口 这里是注册窗口拉

        HWND hwnd;
        hwnd=CreateWindow("sunxin2006","http://www.sunxin.org",WS_OVERLAPPEDWINDOW,
                0,0,600,400,NULL,NULL,hInstance,NULL);
// CreateWindow参数比较多 但是记住常用的就可以啦
//@1    "sunxin2006"             LPCTSTR lpClassName,就是前面自己写的sunxin2006啦
//@2    "http://www.sunxin.org"               LPCTSTR lpWindowName, 显现在窗口左上角的标题 这里是http://www.sunxin.org
//@3    WS_OVERLAPPEDWINDOW      注意区别上边的 wndclass里面的style 两者的参数是不一样的 这个参数比较多平常用WS_OVERLAPPEDWINDOW就可以了
//@4    x,y,nWidth,nHeight
// 0,0,600,400 窗口的x y坐标 以及窗口的宽度和高度可以 将x设置为 CW_USEDEFAULT  y也设置成CW_USEDEFAULT
//同理 将nWidth设置成CW_USEDEFAULT nHeight也设置成CW_USEDEFAULT  就变成系统默认的了
//@5   NULL  HWND hWndParent 指定这个窗口的父窗口句柄 这个程序中我们没有父窗口 填NULL就可以
//@6   NULL  HMENU hMenu 指定窗口菜单的句柄 这个程序没有哈哈 填NULL
//@7    hInstance      HINSTANCE hInstance  指定一个窗口的实例句柄这里肯定是hInstance 因为我们这个程序很简单 只有一个hInstance没有别的实例句柄拉
//@8   NULL    LPVOID lpParam     至于是干什么的 暂时不用管我们也是用不到 设为NULL

        ShowWindow(hwnd,SW_SHOWNORMAL);//用CreateWindow创建了一个窗口之后就可以展示窗口了
        //第一个参数就是一个窗口句柄
        //第二个参数值比较多 详细见附加说明
        //UpdateWindow(hwnd);

        MSG msg;//Windows的消息机制一定要好好理解 这里的意思是实例化一个MSG结构 也就是实例化一个消息结构
        while(GetMessage(&msg,NULL,0,0))//使用GetMessage从消息队列里面取出消息
        //@1&msg  既然要获取一个消息自然需要知道消息的家拉 这里是取实例化后的msg的地址
        //@2需要一个句柄 传递NULL默认即可
        //@3第一个消息和最后一个消息 两个都设置为0就是接受一切消息
        {
                TranslateMessage(&msg);//函数功能:该函数将虚拟键消息转换为字符消息。字符消息被寄送到调用线程的消息队列里,
                //当下一次线程调用函数GetMessage或PeekMessage时被读出。
                //注意 转换成了字符消息之后字符消息会被投递到消息队列中 这里有一个进队消息和不进队消息 写在附加说明中
       
            DispatchMessage(&msg);//分发消息 在这里也就是分发消息到我们定义的消息处理函数WinSunProc中
        }
        return msg.wParam;
}

//重点呐  这个函数要自己写滴 这就是传说中的消息处理函数啦
LRESULT CALLBACK WinSunProc(
  HWND hwnd,      // handle to window  因为一个程序可以有多个窗口啊 这个hwnd标识了到底接收消息的是哪个窗口
  UINT uMsg,      // message identifier
  WPARAM wParam,  // first message parameter 这个参数就是你的按键产生的ASCII值  比方说我按F会产生 102  那么你按了一下F这参数的值就是102喽
  LPARAM lParam   // second message parameter
)
{//C语言中的switch case语句在GUI程序中的具体应用 忘了的童鞋自己复习一下
        switch(uMsg)
        {
        case WM_CHAR:
                char szChar[20];
                sprintf(szChar,"char code is %d",wParam);//int sprintf( char *buffer, const char *format [, argument] ... );
                MessageBox(hwnd,szChar,"char",0);//句柄 消息框里的内容 消息框的标题 消息框样式0代表默认
                break;
        case WM_LBUTTONDOWN:
                MessageBox(hwnd,"mouse clicked","message",0);
                HDC hdc;
                hdc=GetDC(hwnd);
                TextOut(hdc,0,50,"程序员之家",strlen("程序员之家"));//在指定位置输出一行数字 试着可以把0换成50看看 这行字的位置变了哦
                ReleaseDC(hwnd,hdc);  //千万不要忘了 内存泄漏 啊
                break;
        case WM_PAINT:
                HDC hDC;//实例化hDC的时候DC就已经占内存了
                PAINTSTRUCT ps;
                hDC=BeginPaint(hwnd,&ps);//BeginPaint只能在WM_PAINT消息中使用 结束后一定要用EndPaint去释放BeginPaint得到的DC
                TextOut(hDC,0,0,"http://www.sunxin.org",strlen("http://www.sunxin.org"));
                EndPaint(hwnd,&ps);
                break;
        case WM_CLOSE:
                if(IDYES==MessageBox(hwnd,"是否真的结束?","message",MB_YESNO))
                {
                        DestroyWindow(hwnd);
                }
                //窗口的退出过程是这样的 用户点击叉号 会产生WM_CLOSE消息 然后被捕获到 如果选择真的结束 那么 接着调用DestroyWindow销毁窗口
                //当销毁窗口完成后会向窗口发送WM_DESTROY消息 然后调用PostQuitMessage函数彻底退出程序
                break;
        case WM_DESTROY:
                PostQuitMessage(0);
                break;
        default:
                return DefWindowProc(hwnd,uMsg,wParam,lParam);//我们不出力的消息默认交给DefWindowProc 系统提供的这个默认的函数处理就好了
        }
        return 0;
}

/*附加说明
@0几个函数在MSDN中的定义 我的msdn是01年VC6.0最后一版本的msdn
第一个是WNDCLASS结构体
typedef struct _WNDCLASS {
    UINT       style;
    WNDPROC    lpfnWndProc;
    int        cbClsExtra;
    int        cbWndExtra;
    HINSTANCE  hInstance;
    HICON      hIcon;
    HCURSOR    hCursor;
    HBRUSH     hbrBackground;
    LPCTSTR    lpszMenuName;
    LPCTSTR    lpszClassName;
} WNDCLASS, *PWNDCLASS;
第二个是CreateWindow在MSDN中的定义
HWND CreateWindow(
  LPCTSTR lpClassName,  // registered class name
  LPCTSTR lpWindowName, // window name
  DWORD dwStyle,        // window style
  int x,                // horizontal position of window
  int y,                // vertical position of window
  int nWidth,           // window width
  int nHeight,          // window height
  HWND hWndParent,      // handle to parent or owner window
  HMENU hMenu,          // menu handle or child identifier
  HINSTANCE hInstance,  // handle to application instance
  LPVOID lpParam        // window-creation data
);
第三个是GetMessage
BOOL GetMessage(
  LPMSG lpMsg,         // message information
  HWND hWnd,           // handle to window
  UINT wMsgFilterMin,  // first message
  UINT wMsgFilterMax   // last message
);
第四个是MessageBox
int MessageBox(
  HWND hWnd,          // handle to owner window
  LPCTSTR lpText,     // text in message box
  LPCTSTR lpCaption,  // message box title
  UINT uType          // message box style
);
第五个是
BOOL TextOut(
  HDC hdc,           // handle to DC
  int nXStart,       // x-coordinate of starting position
  int nYStart,       // y-coordinate of starting position
  LPCTSTR lpString,  // character string
  int cbString       // number of characters
);
第六个
typedef struct tagPAINTSTRUCT {
  HDC  hdc;
  BOOL fErase;
  RECT rcPaint;
  BOOL fRestore;
  BOOL fIncUpdate;
  BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT;

@1关于强制转换
WNDCLASS 这个结构中的
wndclass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
此处有一个强制转换(HBRUSH)
为什么一定要加一个这个呢?
希望大大们给个通俗易懂的解释。谢谢各位。

回复:(HBRUSH)GetStockObject(WHITE_BRUSH)前面为什么要加(HBRUSH)

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

GetStockObject返回值类型为HGDIOBJ,而这里需要的类型为HBRUSH,
所以强制转换。

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

GetStockObject返回的是一个gdi句柄,gdi句柄包括pen, brush, font和palette。
因此如果是Brush句柄, 当然要强制转换了。

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

最简单的解释
不强制转一下的编不过去
当然有的编译器就是报一个warning

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

wndclass.hbrBackground=HBRUSH(GetStockObject(WHITE_BRUSH));
也是可以的。

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

GetStockObject()返回值类型 是HGDIOBJ HGDIOBJ 就是VOID * 无类型的指针,所以使用需要强转

--------------------------------------------------------------------------------
@2 注意区别上边的 wndclass里面的style

wndclass.style
      在msdn上找了好久都找不到中文帮助,可恨的是c#的帮助全是中文的。这我就不能理解了,难不成c++要低一个档次?没办法,只好用博客来做收藏了~~
      窗口风格:
WS_OVERLAPPEDWINDOW指定一个具有所有标准控件的窗口,是一个组合体。简单的说,一个可以最大化、最小化、随意改变大小等等地窗口;
WS_OVERLAPPED指定一个具有标题栏和边界的重叠窗口,一个具有标题栏、可改变大小的窗口;
WS_POPUP指定一个弹出的窗口,一个光秃秃的窗口;
WS_VISIBLE指定一个初始时可见的窗口,一个黑色的大方框,可能你要用它写一个全屏的游戏,选择;

窗口类风格:
CS_HREDRAW:一旦移动或尺寸调整使客户区的宽度发生变化,就重新绘制窗口;
CS_VREDRAW:一旦移动或尺寸调整使客户区的高度发生变化,就重新绘制窗口;
CS_OWNDC:为该类中的每一个窗口分配一个唯一的设备上下文;
CS_DBLCLKS:当用户双击鼠标时向窗口过程发送双击消息;

@3ShowWindow函数的参数说明
ShowWindow
hWnd:指窗口句柄。   nCmdShow:指定窗口如何显示。如果发送应用程序的程序提供了STARTUPINFO结构,则应用程序第一次调用ShowWindow时该参数被忽略。
否则,在第一次调用ShowWindow函数时,该值应为在函数WinMain中nCmdShow参数。在随后的调用中,该参数可以为下列值之一:   
SW_FORCEMINIMIZE:在WindowNT5.0中最小化窗口,即使拥有窗口的线程被挂起也会最小化。在从其他线程最小化窗口时才使用这个参数。   
SW_HIDE:隐藏窗口并激活其他窗口。   
SW_MAXIMIZE:最大化指定的窗口。   
SW_MINIMIZE:最小化指定的窗口并且激活在Z序中的下一个顶层窗口。   
SW_RESTORE:激活并显示窗口。如果窗口最小化或最大化,则系统将窗口恢复到原来的尺寸和位置。在恢复最小化窗口时,应用程序应该指定这个标志。   
SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。   
SW_SHOWDEFAULT:依据在STARTUPINFO结构中指定的SW_FLAG标志设定显示状态,STARTUPINFO 结构是由启动应用程序的程序传递给CreateProcess函数的。   
SW_SHOWMAXIMIZED:激活窗口并将其最大化。   
SW_SHOWMINIMIZED:激活窗口并将其最小化。   
SW_SHOWMINNOACTIVATE:窗口最小化,激活窗口仍然维持激活状态。   
SW_SHOWNA:以窗口原来的状态显示窗口。激活窗口仍然维持激活状态。   
SW_SHOWNOACTIVATE:以窗口最近一次的大小和状态显示窗口。激活窗口仍然维持激活状态。   
SW_SHOWNORMAL:激活并显示一个窗口。如果窗口被最小化或最大化,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。

  如果窗口以前可见,则返回值为非零。如果窗口以前被隐藏,则返回值为零。
@4进队消息不进队消息
不进队消息和进队消息。不进队消息是指由Windows直接调用消息处理函数,把消息直接交给其处理。而进队消息是指Windows将消息放入到程序中的消息队列中取,
并通过程序中的消息循环,循环把消息取出,经过一定处理(如例子中经过translate),然后由函数DispathMessage函数将消息分发给消息处理函数处理。
进队消息基本上是用户的输入:击键的消息(WM_KEYDOWN、WM_KEYUP)键盘输入产生字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)、
鼠标键(WM_LBUTTONDOWN)、计时消息(WM_TIMER)、刷新消息(WM_PAINT)和退出消息(WM_QUIT)。不进队消息则是其他消息。一般情况下,
不进队消息的产生是由于调用了其他Windows函数。如,当调用CreateWindow时,Windows将创建WM_CREATE消息、当调用ShowWindow时,
将产生WM_SIZE和WM_SHOWWINDOW消息、当调用UpdateWindow时创建的WM_PAINT消息(注意,并不是某个类型是进队消息就永远是进队消息,
如WM_PAINT有进队的,也有不进队的)、还有其他进队消息也有可能在不进队消息中出现,整个处理过程是复杂的,但由于Windows已经解决大部分的问题,
因此我们可以认为我们获得的消息是有序的、同步的。

发送消息:SendMessage 和 PostMessage,SendMessage为发送“不进队消息”,直接调用处理函数处理,返回处理函数处理结果。PostMessage为发送“进队消息”。
PostThreadMessage为向线程发消息。
*/
////把整数123 打印成一个字符串保存在s 中。   sprintf(s, "%d", 123); //产生"123"
/*sprintf函数的使用
#include "stdio.h"
#include "iostream.h"
void main()
{
         int wParam=9;
        char szChar[20];
    sprintf(szChar,"char code is %d",wParam);//格式化字符串并把字符串保存在一个变量中 这个函数容易引起缓冲区溢出 以及内存泄漏 不推荐使用
        cout<<szChar[1]<<endl;
        cout<<szChar<<endl;
        printf("szChar= %d \n",szChar[20]);
};
MessageBox函数的使用
#include "stdafx.h"        // 这是VC自动添加的头文件,没有什么用途
#include <windows.h>        // 包含MessageBox函数声明的头文件

int main(int argc, char* argv[])
{
        // 调用API函数MessageBox
        int nSelect = ::MessageBox(NULL, "Hello, Windows XP", "Greetings", MB_OKCANCEL);
        if(nSelect == IDOK)
                printf(" 用户选择了“确定”按钮 \n");
        else
                printf(" 用户选择了“取消”按钮 \n");
        return 0;
}

缓冲区溢出的C函数
C 中大多数缓冲区溢出问题可以直接追溯到标准 C 库。最有害的罪魁祸首是不进行自变量检查的、有问题的字符串操作(strcpy、strcat、sprintf 和 gets)。
一般来讲,象“避免使用 strcpy()”和“永远不使用 gets()”这样严格的规则接近于这个要求。

今天,编写的程序仍然利用这些调用,因为从来没有人教开发人员避免使用它们。某些人从各处获得某个提示,但即使是优秀的开发人员也会被这弄糟。
他们也许在危险函数的自变量上使用自己总结编写的检查,或者错误地推论出使用潜在危险的函数在某些特殊情况下是“安全”的。

第一位公共敌人是 gets()。永远不要使用 gets()。该函数从标准输入读入用户输入的一行文本,它在遇到 EOF 字符或换行字符之前,不会停止读入文本。
也就是:gets() 根本不执行边界检查。因此,使用 gets() 总是有可能使任何缓冲区溢出。作为一个替代方法,可以使用方法 fgets()。
它可以做与 gets() 所做的同样的事情,但它接受用来限制读入字符数目的大小参数,因此,提供了一种防止缓冲区溢出的方法。例如,不要使用以下代码:

void main()
  {
  char buf[1024];
  gets(buf);
  }

而使用以下代码:

#define BUFSIZE 1024

void main()
  {
  char buf[BUFSIZE];
  fgets(buf, BUFSIZE, stdin);
  }

C 编程中的主要陷阱

C 语言中一些标准函数很有可能使您陷入困境。但不是所有函数使用都不好。通常,利用这些函数之一需要任意输入传递给该函数。这个列表包括:

strcpy()
strcat()
sprintf()
scanf()
sscanf()
fscanf()
vfscanf()
vsprintf
vscanf()
vsscanf()
streadd()
strecpy()
strtrns()
坏消息是我们推荐,如果有任何可能,避免使用这些函数。好消息是,在大多数情况下,都有合理的替代方法。我们将仔细检查它们中的每一个,
所以可以看到什么构成了它们的误用,以及如何避免它。

strcpy()函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目。复制字符的数目直接取决于源字符串中的数目。如果源字符串碰巧来自用户输入,
且没有专门限制其大小,则有可能会陷入大的麻烦中!

如果知道目的地缓冲区的大小,则可以添加明确的检查:

if(strlen(src) >= dst_size) {

  }
       else {
  strcpy(dst, src);

完成同样目的的更容易方式是使用 strncpy() 库例程:

strncpy(dst, src, dst_size-1);
  dst[dst_size-1] = '\0';

如果 src 比 dst 大,则该函数不会抛出一个错误;当达到最大尺寸时,它只是停止复制字符。注意上面调用 strncpy() 中的 -1。如果 src 比 dst 长,
则那给我们留有空间,将一个空字符放在 dst 数组的末尾。

当然,可能使用 strcpy() 不会带来任何潜在的安全性问题,正如在以下示例中所见:

strcpy(buf, "Hello!");

即使这个操作造成 buf 的溢出,但它只是对几个字符这样而已。由于我们静态地知道那些字符是什么,并且很明显,由于没有危害,所以这里无须担心 ― 当然,
除非可以用其它方式覆盖字符串“Hello”所在的静态存储器。

确保 strcpy() 不会溢出的另一种方式是,在需要它时就分配空间,确保通过在源字符串上调用 strlen() 来分配足够的空间。例如:

dst = (char *)malloc(strlen(src));
  strcpy(dst, src);

strcat()函数非常类似于 strcpy(),除了它可以将一个字符串合并到缓冲区末尾。它也有一个类似的、更安全的替代方法 strncat()。如果可能,
使用 strncat() 而不要使用 strcat()。

函数 sprintf()和 vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。换句话说,
使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。例如,考虑以下代码:

void main(int argc, char **argv)
  {
  char usage[1024];
  sprintf(usage, "USAGE: %s -f flag [arg1]\n", argv[0]);
  }

我们经常会看到类似上面的代码。它看起来没有什么危害。它创建一个知道如何调用该程序字符串。那样,可以更改二进制的名称,
该程序的输出将自动反映这个更改。 虽然如此, 该代码有严重的问题。文件系统倾向于将任何文件的名称限制于特定数目的字符。
那么,您应该认为如果您的缓冲区足够大,可以处理可能的最长名称,您的程序会安全,对吗?只要将 1024 改为对我们的操作系统适合的任何数目,
就好了吗?但是,不是这样的。通过编写我们自己的小程序来推翻上面所说的,可能容易地推翻这个限制:

void main()
  {
  execl("/path/to/above/program",
  <<insert really long string here>>,
  NULL);
  }

函数 execl() 启动第一个参数中命名的程序。第二个参数作为 argv[0] 传递给被调用的程序。我们可以使那个字符串要多长有多长!

那么如何解决 {v}sprintf() 带来得问题呢?遗憾的是,没有完全可移植的方法。某些体系结构提供了 snprintf() 方法,
即允许程序员指定将多少字符从每个源复制到缓冲区中。例如,如果我们的系统上有 snprintf,则可以修正一个示例成为:

void main(int argc, char **argv)
  {
  char usage[1024];
  char format_string = "USAGE: %s -f flag [arg1]\n";
  snprintf(usage, format_string, argv[0],
  1024-strlen(format_string) + 1);
  }

注意,在第四个变量之前,snprintf() 与 sprintf() 是一样的。第四个变量指定了从第三个变量中应被复制到缓冲区的字符最大数目。
注意,1024 是错误的数目!我们必须确保要复制到缓冲区使用的字符串总长不超过缓冲区的大小。所以,必须考虑一个空字符,
加上所有格式字符串中的这些字符,再减去格式说明符 %s。该数字结果为 1000, 但上面的代码是更具有可维护性,因为如果格式字符串偶然发生变化,它不会出错。

{v}sprintf() 的许多(但不是全部)版本带有使用这两个函数的更安全的方法。可以指定格式字符串本身每个自变量的精度。
例如,另一种修正上面有问题的 sprintf() 的方法是:

void main(int argc, char **argv)
  {
  char usage[1024];
  sprintf(usage, "USAGE: %.1000s -f flag [arg1]\n", argv[0]);
  }

注意,百分号后与 s 前的 .1000。该语法表明,从相关变量(本例中是 argv[0])复制的字符不超过 1000 个。

如果任一解决方案在您的程序必须运行的系统上行不通,则最佳的解决方案是将 snprintf() 的工作版本与您的代码放置在一个包中。
可以找到以 sh 归档格式的、自由使用的版本;请参阅 参考资料。

继续, scanf系列的函数也设计得很差。在这种情况下,目的地缓冲区会发生溢出。考虑以下代码:

void main(int argc, char **argv)
  {
  char buf[256];
  sscanf(argv[0], "%s", &buf);
  }

如果输入的字大于 buf 的大小,则有溢出的情况。幸运的是,有一种简便的方法可以解决这个问题。考虑以下代码,它没有安全性方面的薄弱环节:

void main(int argc, char **argv)
  {
  char buf[256];
  sscanf(argv[0], "%255s", &buf);
  }

百分号和 s 之间的 255 指定了实际存储在变量 buf 中来自 argv[0] 的字符不会超过 255 个。其余匹配的字符将不会被复制。

接下来,我们讨论 streadd()和 strecpy()。由于,不是每台机器开始就有这些调用,那些有这些函数的程序员,在使用它们时,应该小心。
这些函数可以将那些含有不可读字符的字符串转换成可打印的表示。例如,考虑以下程序:

#include <libgen.h>

void main(int argc, char **argv)
  {
  char buf[20];
  streadd(buf, "\t\n", "");
  printf(%s\n", buf);
  }

该程序打印:

\t\n

而不是打印所有空白。如果程序员没有预料到需要多大的输出缓冲区来处理输入缓冲区(不发生缓冲区溢出),
则 streadd() 和 strecpy() 函数可能有问题。如果输入缓冲区包含单一字符 ― 假设是 ASCII 001(control-A)― 则它将打印成四个字符“\001”。
这是字符串增长的最坏情况。如果没有分配足够的空间,以至于输出缓冲区的大小总是输入缓冲区大小的四倍,则可能发生缓冲区溢出。

另一个较少使用的函数是 strtrns(),因为许多机器上没有该函数。函数 strtrns() 取三个字符串和结果字符串应该放在其内的一个缓冲区,
作为其自变量。第一个字符串必须复制到该缓冲区。一个字符被从第一个字符串中复制到缓冲区,除非那个字符出现在第二个字符串中。
如果出现的话,那么会替换掉第三个字符串中同一索引中的字符。这听上去有点令人迷惑。让我们看一下,将所有小写字符转换成大写字符的示例:

#include <libgen.h>

void main(int argc, char **argv)
  {
  char lower[] = "abcdefghijklmnopqrstuvwxyz";
  char upper[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  char *buf;
  if(argc < 2) {
  printf("USAGE: %s arg\n", argv[0]);
  exit(0);
  } buf = (char *)malloc(strlen(argv[1]));
  strtrns(argv[1], lower, upper, buf);
  printf("%s\n", buf);
  }

以上代码实际上不包含缓冲区溢出。但如果我们使用了固定大小的静态缓冲区,而不是用 malloc() 分配足够空间来复制 argv[1],则可能会引起缓冲区溢出情况。

回页首

避免内部缓冲区溢出

realpath() 函数接受可能包含相对路径的字符串,并将它转换成指同一文件的字符串,但是通过绝对路径。在做这件事时,它展开了所有符号链接。

该函数取两个自变量,第一个作为要规范化的字符串,第二个作为将存储结果的缓冲区。当然,需要确保结果缓冲区足够大,以处理任何大小的路径。
分配的 MAXPATHLEN 缓冲区应该足够大。然而,使用 realpath() 有另一个问题。如果传递给它的、要规范化的路径大小大于 MAXPATHLEN
,则 realpath() 实现内部的静态缓冲区会溢出!虽然实际上没有访问溢出的缓冲区,但无论如何它会伤害您的。结果是,应该明确不使用 realpath(),
除非确保检查您试图规范化的路径长度不超过 MAXPATHLEN。

其它广泛可用的调用也有类似的问题。经常使用的 syslog() 调用也有类似的问题,直到不久前,才注意到这个问题并修正了它。
大多数机器上已经纠正了这个问题,但您不应该依赖正确的行为。最好总是假定代码正运行在可能最不友好的环境中,只是万一在哪天它真的这样
getopt() 系列调用的各种实现,以及 getpass() 函数,都可能产生内部静态缓冲区溢出问题。如果您不得不使用这些函数,
最佳解决方案是设置传递给这些函数的输入长度的阈值。

自己模拟 gets() 的安全性问题以及所有问题是非常容易的。 例如,下面这段代码:

char buf[1024];
  int i = 0;
  char ch;
  while((ch = getchar()) != '\n')
  {
  if(ch == -1) break;
  buf[i++] = ch;
  }

哎呀!可以用来读入字符的任何函数都存在这个问题,包括 getchar()、fgetc()、getc() 和 read()。

缓冲区溢出问题的准则是:总是确保做边界检查。

C 和 C++ 不能够自动地做边界检查,这实在不好,但确实有很好的原因,来解释不这样做的理由。边界检查的代价是效率。一般来讲,
C 在大多数情况下注重效率。然而,获得效率的代价是,C 程序员必须十分警觉,并且有极强的安全意识,才能防止他们的程序出现问题,
而且即使这些,使代码不出问题也不容易。

在现在,变量检查不会严重影响程序的效率。大多数应用程序不会注意到这点差异。所以,应该总是进行边界检查。在将数据复制到您自己的缓冲区之前,
检查数据长度。同样,检查以确保不要将过大的数据传递给另一个库,因为您也不能相信其他人的代码!(回忆一下前面所讨论的内部缓冲区溢出。)

回页首

其它危险是什么?

遗憾的是,即使是系统调用的“安全”版本 ― 譬如,相对于 strcpy() 的 strncpy() ― 也不完全安全。也有可能把事情搞糟。
即使“安全”的调用有时会留下未终止的字符串,或者会发生微妙的相差一位错误。当然,如果您偶然使用比源缓冲区小的结果缓冲区,
则您可能发现自己处于非常困难的境地。

与我们目前所讨论的相比,往往很难犯这些错误,但您应该仍然意识到它们。当使用这类调用时,要仔细考虑。如果不仔细留意缓冲区大小,
包括 bcopy()、fgets()、memcpy()、snprintf()、strccpy()、strcadd()、strncpy() 和 vsnprintf(),许多函数会行为失常。

另一个要避免的系统调用是 getenv()。使用 getenv() 的最大问题是您从来不能假定特殊环境变量是任何特定长度的。
我们将在后续的专栏文章中讨论环境变量带来的种种问题。

到目前为止,我们已经给出了一大堆常见 C 函数,这些函数容易引起缓冲区溢出问题。当然,还有许多函数有相同的问题。特别是,
注意第三方 COTS 软件。不要设想关于其他人软件行为的任何事情。还要意识到我们没有仔细检查每个平台上的每个常见库(我们不想做那一工作),
并且还可能存在其它有问题的调用。

即使我们检查了每个常见库的各个地方,如果我们试图声称已经列出了将在任何时候遇到的所有问题,则您应该持非常非常怀疑的态度。
我们只是想给您起一个头。其余全靠您了。

回页首

静态和动态测试工具

我们将在以后的专栏文章中更加详细地介绍一些脆弱性检测的工具,但现在值得一提的是两种已被证明能有效帮助找到和去除缓冲区溢出问题的扫描工具。
这两个主要类别的分析工具是静态工具(考虑代码但永不运行)和动态工具(执行代码以确定行为)。

可以使用一些静态工具来查找潜在的缓冲区溢出问题。很糟糕的是,没有一个工具对一般公众是可用的!许多工具做得一点也不比自动化 grep 命令多,
可以运行它以找到源代码中每个有问题函数的实例。由于存在更好的技术,这仍然是高效的方式将几万行或几十万行的大程序缩减到只有数百个“潜在的问题”。
(在以后的专栏文章中,将演示一个基于这种方法的、草草了事的扫描工具,并告诉您有关如何构建它的想法。)

较好的静态工具利用以某些方式表示的数据流信息来断定哪个变量会影响到其它哪个变量。用这种方法,可以丢弃来自基于 grep 的分析的某些“假肯定”。
David Wagner 在他的工作中已经实现了这样的方法(在“Learning the basics of buffer overflows”中描述;请参阅 参考资料),
在 Reliable Software Technologies 的研究人员也已实现。当前,数据流相关方法的问题是它当前引入了假否定(即,它没有标志可能是真正问题的某些调用)。

第二类方法涉及动态分析的使用。动态工具通常把注意力放在代码运行时的情况,查找潜在的问题。一种已在实验室使用的方法是故障注入。
这个想法是以这样一种方式来检测程序:对它进行实验,运行“假设”游戏,看它会发生什么。
有一种故障注入工具 ― FIST(请参阅 参考资料)已被用来查找可能的缓冲区溢出脆弱性。

最终,动态和静态方法的某些组合将会给您的投资带来回报。但在确定最佳组合方面,仍然有许多工作要做。

回页首

Java 和堆栈保护可以提供帮助

如上一篇专栏文章中所提到的(请参阅 参考资料),堆栈捣毁是最恶劣的一种缓冲区溢出攻击,特别是,当在特权模式下捣毁了堆栈。
这种问题的优秀解决方案是非可执行堆栈。 通常,利用代码是在程序堆栈上编写,并在那里执行的。(我们将在下一篇专栏文章中解释这是如何做到的。)
获取许多操作系统(包括 Linux 和 Solaris)的非可执行堆栈补丁是可能的。(某些操作系统甚至不需要这样的补丁;它们本身就带有。)

非可执行堆栈涉及到一些性能问题。(没有免费的午餐。)此外,在既有堆栈溢出又有堆溢出的程序中,它们易出问题。可以利用堆栈溢出使程序跳转至利用代码
,该代码被放置在堆上。 没有实际执行堆栈中的代码,只有堆中的代码。这些基本问题非常重要,我们将在下一篇专栏文章中专门刊载。

当然,另一种选项是使用类型安全的语言,譬如 Java。较温和的措施是获取对 C 程序中进行数组边界检查的编译器。对于 gcc 存在这样的工具。
这种技术可以防止所有缓冲区溢出,堆和堆栈。不利的一面是,对于那些大量使用指针、速度是至关重要的程序,这种技术可能会影响性能。
但是在大多数情况下,该技术运行得非常好。

Stackguard 工具实现了比一般性边界检查更为有效的技术。它将一些数据放在已分配数据堆栈的末尾,并且以后会在缓冲区溢出可能发生前,
查看这些数据是否仍然在那里。这种模式被称之为“金丝雀”。(威尔士的矿工将 金丝雀放在矿井内来显示危险的状况。当空气开始变得有毒时,
金丝雀会昏倒,使矿工有足够时间注意到并逃离。)

Stackguard 方法不如一般性边界检查安全,但仍然相当有用。Stackguard 的主要缺点是,与一般性边界检查相比,它不能防止堆溢出攻击。
一般来讲,最好用这样一个工具来保护整个操作系统,否则,由程序调用的不受保护库(譬如,标准库)可以仍然为基于堆栈的利用代码攻击打开了大门。

类似于 Stackguard 的工具是内存完整性检查软件包,譬如,Rational 的 Purify。这类工具甚至可以保护程序防止堆溢出,但由于性能开销,
这些工具一般不在产品代码中使用。

回页首

结束语

在本专栏的上两篇文章中,我们已经介绍了缓冲区溢出,并指导您如何编写代码来避免这些问题。我们还讨论了可帮助使您的程序安全远离可怕的缓冲区溢出的几个工具。表 1 总结了一些编程构造,我们建议您小心使用或避免一起使用它们。如果有任何认为我们应该将其它函数加入该列表,请则通知我们,我们将更新该列表。

函数        严重性        解决方案
gets        最危险        使用 fgets(buf, size, stdin)。这几乎总是一个大问题!
strcpy        很危险        改为使用 strncpy。
strcat        很危险        改为使用 strncat。
sprintf        很危险        改为使用 snprintf,或者使用精度说明符。
scanf        很危险        使用精度说明符,或自己进行解析。
sscanf        很危险        使用精度说明符,或自己进行解析。
fscanf        很危险        使用精度说明符,或自己进行解析。
vfscanf        很危险        使用精度说明符,或自己进行解析。
vsprintf        很危险        改为使用 vsnprintf,或者使用精度说明符。
vscanf        很危险        使用精度说明符,或自己进行解析。
vsscanf        很危险        使用精度说明符,或自己进行解析。
streadd        很危险        确保分配的目的地参数大小是源参数大小的四倍。
strecpy        很危险        确保分配的目的地参数大小是源参数大小的四倍。
strtrns        危险        手工检查来查看目的地大小是否至少与源字符串相等。
realpath        很危险(或稍小,取决于实现)        分配缓冲区大小为 MAXPATHLEN。同样,手工检查参数以确保输入参数不超过 MAXPATHLEN。
syslog        很危险(或稍小,取决于实现)        在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getopt        很危险(或稍小,取决于实现)        在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getopt_long        很危险(或稍小,取决于实现)        在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getpass        很危险(或稍小,取决于实现)        在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getchar        中等危险        如果在循环中使用该函数,确保检查缓冲区边界。
fgetc        中等危险        如果在循环中使用该函数,确保检查缓冲区边界。
getc        中等危险        如果在循环中使用该函数,确保检查缓冲区边界。
read        中等危险        如果在循环中使用该函数,确保检查缓冲区边界。
bcopy        低危险        确保缓冲区大小与它所说的一样大。
fgets        低危险        确保缓冲区大小与它所说的一样大。
memcpy        低危险        确保缓冲区大小与它所说的一样大。
snprintf        低危险        确保缓冲区大小与它所说的一样大。
strccpy        低危险        确保缓冲区大小与它所说的一样大。
strcadd        低危险        确保缓冲区大小与它所说的一样大。
strncpy        低危险        确保缓冲区大小与它所说的一样大。
vsnprintf        低危险        确保缓冲区大小与它所说的一样大。
在我们急匆匆讲述这些基础知识时,到现在为止,已经遗漏了一些缓冲区溢出很酷的细节。在下几篇专栏文章中,我们将深入这台“引擎”的工作,
并给它加点黄油。我们将详细地了解缓冲区溢出的工作原理,甚至还会演示一些利用代码。

*/

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 0
支持
分享
最新回复 (9)
雪    币: 4
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
我想知道楼主图片中用的是什么编辑软件。
2011-3-26 16:27
0
雪    币: 673
活跃值: (278)
能力值: ( LV15,RANK:360 )
在线值:
发帖
回帖
粉丝
3
notepad++ 啊
2011-3-26 17:10
0
雪    币: 349
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感觉notepad++ 的字体没这么难看啊
2011-3-26 17:40
0
雪    币: 673
活跃值: (278)
能力值: ( LV15,RANK:360 )
在线值:
发帖
回帖
粉丝
5
那可能是我设置的问题吧  设置难看了可能
2011-3-26 17:42
0
雪    币: 46
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
api写的图形界面? 看不懂帮忙顶
2011-3-26 18:23
0
雪    币: 673
活跃值: (278)
能力值: ( LV15,RANK:360 )
在线值:
发帖
回帖
粉丝
7
你注册的好早哦 怎么不来看雪学习了 楼上的  
看雪牛人好多  发这样的程序之前我都犹豫了好长时间 不会被人嘲笑吧
2011-3-26 18:48
0
雪    币: 1259
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
stu
8
真够详细的。
2011-3-26 18:52
0
雪    币: 128
活跃值: (111)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
Windows程序设计这本书最适合初学者
2011-3-26 19:02
0
雪    币: 673
活跃值: (278)
能力值: ( LV15,RANK:360 )
在线值:
发帖
回帖
粉丝
10
我发现最适合初学者的不是老外写的Windows程序设计 而是王艳平老师的那本Windows程序设计

还有孙鑫老师的VC++深入详解 虽然 讲的不是很深入 在高手看来 但是在我看来 孙鑫老师是我目前遇到的 讲的最好的一个老师  他能把你真真正正的领进门
2011-3-26 19:09
0
游客
登录 | 注册 方可回帖
返回
//