On The Road ZJL

我的博客列表

博客归档

2008年7月31日星期四

(Z)用Expat處理你的XML資料_留在西雅图

(Z)用Expat處理你的XML資料_留在西雅图:http://joepasscheng.googlepages.com/本文出处~~

當你的程式之間需要傳遞資料時,你會怎麼做?譬如說設定檔資料,使用一個最簡單的型式:

『id := value』

那如果設定值很複雜又有層次呢?我甚至還看過透過網路把整個二進位資料結構memory複製到網路去,由接受端宣告一個一模一樣的資料結構在memcpy......(當然,看到這種程式要我處理我會有想自殺的感覺...)。

其實XML是一個很好的規範,他的可擴充性讓程式可以自己去處理自己要的資料,也不會因為多了一個欄位讓天下大亂(我的世界就處在混亂中...ㄏㄏ)。但是要你面對XML這類的複雜結構時,我想很多人看到就感到害怕吧。還好這個世界甚麼不多,Open Source的library最多...ㄏㄏ,Expat是一個很不錯的XML parser library,原則上他是一個C library,而且可以在許多平台使用(好啦...有win32版本...不過我沒用過),Expat是一個stream導向的parser library,也就是說他會從data buffer一直讀進資料然後遇到不同的狀態改變做不同的處理。

經過一番google搜尋後發現Expat的文章不多,只有看到一篇在xml.com的介紹文章,當然expat也有不少wrapper library,譬如Python、Perl、Tcl、C++....等等,你可以直接去使用你習慣的語言的wrapper。這篇文章只是抱持著研究的精神的小小介紹,這裡我們使用C++語言來實作一個簡單的xml parser吧。

使用Expat很簡單,首先當然你必須include相關的header file,Expat只會將兩個header file放進你的系統,分別是expat.h與expat_external.h(這裡以Expat2.0.0為例)。其實你只要加入expat.h,而 expat_external.h會自動被加入。所以當然你必須有:

#include

在你要使用Expat library的程式碼中。因為這裡我使用C++語言,因此我定義一個自己的類別"MyParser"

class MyParser

{

public:

MyParser(string fname);

virtual ~MyParser();

}

先不要緊張,這目前是一個do-nothing的類別,我在建構式的地方傳入一個XML檔案名稱的參數,我也將parser的動作在初始階段完成,讓我們看看建構式的內容。Expat需要一個XML_Parser的instance在整個parsing階段,因此我們第一步便要宣告一個 XML_Parser然後建立它:

XML_Parser parser;

parser = XML_ParserCreate(NULL);

if (!parser) {

cout << "create XML_Parser error" << endl;

return 0;

}

Expat使用callback function來處理狀態改變的動作,所以當你需要對某些tag做處理的時候你必須要設定相關的處理函式,這裡我們只對最簡單的型式做處理,因此我只設定start tag、end tag與default handler:

::XML_SetUserData(parser, this);

::XML_SetElementHandler(parser, expatStart, expatEnd);

::XML_SetDefaultHandler(parser, expatHandler);

你可以使用this指標將自己傳給expat當成UserData;再來我們必須在類別內宣告相關的member function,因此我們的類別便宣告成:

class MyParser

{

public:

MyParser(string fname);

virtual ~MyParser();

static void expatStart(void *data, const XML_Char *el, const XML_Char **attr);

static void expatEnd(void *data, const XML_Char *el);

static void expatHandler(void *data, const XML_Char *s, int len);

}

你可以看到為了讓Expat的C library當成callback function我們將其宣告成static的member function,而當你要用類別內的成員變數或函式時,你不能直接呼叫或使用(沒忘記static吧),因此這個時候我們在 XML_SetUserData(parser, this)做的動作便派上用場,你可以看到expatStart、expatEnd與expatHandler中第一個不定指標參數void *data便會是你在XML_SetUserData時傳入的第二個參數。因此假設你在這3個callback function中要使用一個類別函式"do_something()"時,你可以這樣做:

((MyParser *)data)->do_something();

這樣一來,我們基本的宣告便完成,可以開始餵資料給XML_Parser了

file = fopen(fname.c_str(), "r");

do {

len = fread(buf, 1, sizeof(buf), file);

done = len < sizeof(buf);

if (!XML_Parse(parser, buf, len, done))

done = 1;

} while (!done);

::XML_ParserFree(parser);

fclose(file);

當然最後不要忘了釋放掉我們宣告的parser與關閉檔案。一個最簡單的Expat動作流程就是這麼簡單......
讓我們看看這3個callback function裏面的作法;首先我們先看一個簡單的XML檔案如左:








2006

7

7

新店中興路星巴克

Jonny Depp

加勒比海盜....好看





2006

7

15

小張

內湖





這個簡單的XML範例為最基本的XML,它沒有DTD宣告也沒有CDATA之類的,只單純的將資料結構化罷了。那Expat的callback function怎麼運作呢?原則上它幫你分好資料與tag的分別,至於你要如何處理資料或是tag屬性你要自己在callback function完成。舉例來說,我們的expatStart函式便會在每次Expat遇到起始tag時去呼叫,而expatEnd就會在遇到結束的 tag時被呼叫,中間的資料便會呼叫我們的expatHandler。
因為XML是一個巢狀結構化的資料,因此必須記住你還未處理完的tag,這裡我們可以用一個簡單的STL vector來記住tag,簡單的push_back與pop_back就可以幫你記住tag。

因此我們宣告一個
vector working_tag;
這樣便可以在expatStart的地方將tag給push_back而在expatEnd的地方給它pop_back出來

void MyParser::expatStart(void *data, const XML_Char *el, const XML_Char **attr)
{
string tmp = e1;
((MyParser *)data)->working_tag.push_back(tmp);
}

void MyParser::expatEnd(void *data, const XML_Char *el)
{
((MyParser *)data)->working_tag.pop_back();
}

這樣我們便可以知道目前處理的tag是那一個。你可以發現expatStart的函式多了一個參數,那個便是在起始tag裏面的參數,這個二維參數很簡單,就像argv一樣,只差沒告訴你argc,不過沒關係你可以自行判斷。他是兩兩一組,第一個是參數名稱而第二個是參數值,因此一個簡單的loop可以找出所有參數。

for (int i=0; attr[i]; i+=2)
{
string attr_name = attr[i];
string attr_value = attr[i+1];

/* 對你的參數做你希望的處理 */
}

最後是我們的預設處理函式ExpatHandler的部份,因為我們已經將目前處理的tag紀錄起來,因此我們可以由vector得到作用中的tag

void MyParser::expatHandler(void *data, const XML_Char *s, int len)
{
if (((MyParser *)data)->working_tag.size() > 0)
{
vector::iterator iter = ((MyParser *)data)->working_tag.end();
iter--;
/* 對你每一個tag值(*iter)去作你希望的動作 */
}
}

這裡我們對於不是在start tag與end tag裏面的值不去處理,不過真的完善的XML parser必須考慮多一點點,例如此例中的會被當成不在起始與結束tag之間的值而被我們忽略。處理函式的第二個參數是要處理的值,不過注意的是它不會是以null結束的字串給你,但是第三個參數將告訴你他的長度,因此你可以很簡單的處理它

string str = s;
str = str.substr(0, len);

另一個要注意的是Expat會以每一行去處理一次,也就是說如果XML資料為了讓人容易閱讀而使用了換行字元與空白字元,Expat都會將其視為資料。這邊我們可以用最粗糙的方式處理它

int i;
for (i=0; i if (str[i] >= 33)
break;
str = str.substr(i, str.length()-i);

這樣我們把特殊自元處理掉後就可以得到真正的值去處理了。一個XML parser用Expat處理起來是不是非常簡單呢,下次你希望用到XML的資料時,不妨試試Expat吧。"

Paramecium: 編譯 Boost 1.35.0 (Visual Studio 2005 (VC 8.0) + Windows XP)

Paramecium: 編譯 Boost 1.35.0 (Visual Studio 2005 (VC 8.0) + Windows XP): 編譯 Boost 1.35.0 (Visual Studio 2005 (VC 8.0) + Windows XP)
簡介

1. Boost 的原始碼可以在官網 http://www.boost.org 找到,或是直接到 sourceforge 下載,網址是:http://tinyurl.com/m7jqo
2. Boost 有著自己一套的建置系統,叫做 Boost.Build [1],可用在編譯、 安裝、測試等功能上。Boost.Build 則是架構於 Boost.Jam, Boost.Jam 又是 Perforce Jam[2] 的衍生,更詳細的介紹可以在 [3] 看到。
3. 通常在 Windows 上安裝 Boost ,最快速、簡易的方法是去 Boost Consulting 下載 installer [4],不過到今天為止, Boost 1.35.0 的 installer 都還沒 release。所以大家來學學自己編譯吧 : )
4. Boost Getting Started on Windows [5] 是一篇官方的教學文件,有簡易的 bjam 使用方法,但無註明完整安裝的步驟,另外它也簡介了Boost Project 的 layout, simple usage 等等。
5. Boost 身為最先進、標準的 C++ library ,它大量使用了 generic programming, metaprogramming 等技巧,使得實際上需要編譯成函式庫(.lib, .dll)的部份反而少了,根據 Boost Getting Started on Windows [5] 的說明,只有以下幾個 library 使用時,一定需要先編譯成函式庫(.lib, .dll):
1. Boost.Filesystem
2. Boost.IOStreams
3. Boost.ProgramOptions
4. Boost.Python
5. Boost.Regex
6. Boost.Serialization
7. Boost.Signals
8. Boost.Thread
9. Boost.Wave

還有幾個則是 optional:
1. Boost.DateTime
2. Boost.Graph
3. Boost.Test

安裝步驟

1. 下載 source code 並解壓縮。
2. 使用 Open Visual Studio 2005 Command Prompt 進入 cmd。利用 Open Visual Studio 2005 Command Prompt 的原因有兩點:
1. 方便呼叫 namke, cl 等 vc toolchain。
2. 若是電腦上安裝多個編譯器,使用 Open Visual Studio 2005 Command Prompt 通常可透過環境變數,讓 bjam 使用正確的 toolchain。
3. 取得 bjam.exe。
1. 可以選擇到 Boost 提供的預先編譯好的 binary,可在http://tinyurl.com/2q36f 下載。
2. 或是自行編譯,以下是編譯步驟:
1. $> cd your_dir\boost_1_35_0\tools\jam\src
2. $> build.bat
3. 執行完後,boost_1_35_0\tools\jam\src\bin.ntx86 下就放著編譯完後的 bjam.exe 了!
4. 將上一個步驟的 bjam.exe 放入 PATH 環境變數或是使用絕對路徑呼叫 bjam,並搭配以下的 options 就可開始編譯並安裝 boost:
* $> bjam.exe --build-dir="your_dir\boost_1_35_0" --build-type=complete --toolset=msvc install
* --build-dir: boost 解壓縮的檔案,記得 = 前後不可有空白!
* --build-type: complete 會產生 release, debug ,以及各種設定的對應:如 multithread, debug, 等等。
* install: 編譯完後,將 header, lib 安裝到指定目錄,預設目錄是:C:\Boost,大小約 2 G。
5. 更多的客製化安裝,可以使用 bjam --help 查看 options。
* bjam 是個有趣的東西,當我在 your_idr\boost_1_35_0\tools\jam\src\bin.ntx86 下執行 bjam --help 時,它跑出的訊息並不多,而切換到 your_idr\boost_1_35_0 時卻又是另一回事,它跑出豐富的 Boost 的編譯資訊,後來發現它會去讀取 Jamroot 這個檔案,這是格外要注意的一點。

Reference

1. Boost.Build: http://www.boost.org/doc/libs/1_35_0/tools/build/v2/index.html
2. Perforce Jam: http://www.perforce.com/jam/jam.html
3. Install Boost: http://www.boost.org/doc/libs/1_35_0/doc/html/bbv2/installation.html
4. Free Download on Boost Consulting: http://www.boost-consulting.com/products/free
5. Boost Getting Started on Windows: http://www.boost.org/doc/libs/1_35_0/more/getting_started/windows.html
6. Boost 提供的 bjam binary 下載點(包含許多種平台): http://tinyurl.com/2q36f
7. Boost Source on Sourceforge: http://tinyurl.com/m7jqo

張貼者: Keiko 位於 3:09 下午

2008年7月30日星期三

VC知识库文章 - STL 字符串类与 UNICODE 及其它......

VC知识库文章 - STL 字符串类与 UNICODE 及其它......

[ 翻译文档 本文适合中级读者 已阅读19605次 ]

C++ Q&A 专栏...

原著:Paul DiLascia
翻译:James Liu

原文出处:MSDN Magazine Aug 2004 (C++ Q&A)
原代码下载: CQA0408.exe (234KB)

1. GetKeyState 使用示例
2. STL 字符串类与 UNICODE
3. 如何向C#或.NET框架暴露 C++ 对象?
4. 如何获取专用文件夹的路径名?

我想让用户双击程序图标时按住 Control 键,以一种特殊的方式来启动程序。 但::GetCommandLine 和__argc 均没有任何反应,用 MFC 中的 CCommandLineInfo 似乎也是如此。有没有一种方法可以解决这个问题呢?
Dean Jones
有,非常简单。你所要做的就是调用 GetKeyState。当你正在处理的当前消息被发送时, 该函数返回虚拟键的状态。这个状态可能是弹起,按下,或者套索钉。套索钉用于大写锁定( Caps)和转换锁(Shift Lock),它们可以转换状态。对于一般的 键,如控制键(VK_CONTROL),如果键被按下,则其状态的高位标识位为 1。
  许多的应用程序使用 Control+F8 作为特殊键来启动恢复模式。比如,如果应用程序允许用户 定制工作间,那么Control+F8就可以将其恢复到初始的默认设置,只是在恢复之前一定要让用户进行确认。做的更好一点的话,你可以在单独的INI文件中保存用户的设置,这样用户 有机会恢复它们。不管怎样,要想在程序启动时检查 Control 键,你可以像下面这样写:

if (GetKeyState(VK_CONTROL)<0)
{
// enter special mode
}

Figure 1 给出了一个基于 MFC 的示例程序代码段,你可以通过本文顶端的链接进行下载,如果用户在启动程序的时候按下Ctrl+F8,它将显示一个消息框,并且 发出蜂鸣声。如果你只是想检查 Control键,可以忽略对 VK_F8 键的测试。

我经常在 C++ 程序中使用标准模板库(STL)的 std::string 类,但在 使用 Unicode 时碰到了问题。在使用常规 C 风格的字符串时,我可以使用 TCHAR 和 _T 宏,这样针对 Unicode 或 ASCII 均可以进行编译,但我 总是发现这种ASCII/Unicode的结合很难与 STL 的 string 类一起使用。你有什么好的建议吗?
Naren J.
是的,一旦知道 TCHAR 和_T 是如何工作的,那么这个问题很简单。基本思想是 TCHAR 要么是char,要么是 wchar_t,这取决于 _UNICODE 的值:

// abridged from tchar.h
#ifdef _UNICODE
typedef wchar_t TCHAR;
#define __T(x) L ## x
#else
typedef char TCHAR;
#define __T(x) x
#endif

  当你在工程设置中选择 Unicode 字符集时,编译器会用 _UNICODE 定义进行编译。如果你选择MBCS(多字节字符集),则编译器将不会带 _UNICODE 定义 。一切取决于_UNICODE 的值。同样,每一个使用字符指针的 Windows API 函数会有一个 A(ASCII) 和一个 W(Wide/Unicode) 版本,这些版本的 实际定义也是根据 _UNICODE 的值来决定:

#ifdef UNICODE
#define CreateFile CreateFileW
#else
#define CreateFile CreateFileA
#endif

  同样,_tprintf 和 _tscanf 对应于 printf 和 scanf。所有带"t"的版本使用 TCHARs 取代了chars。那么怎样把以上的这些应用到 std::string 上呢?很简单。STL已经有一个使用宽字符定义的wstring类 (在 xstring 头文件中定义)。string 和 wstring 均是使用 typedef 定义的模板类,基于 basic_string,用它可以创建任何字符类型的字符串类。以下就是 STL 定义的 string 和 wstring:

// (from include/xstring)
typedef basic_string< char,
char_traits< char >, allocator< char > >
string;
typedef basic_string< wchar_t,
char_traits< wchar_t >, allocator< wchar_t > >
wstring;

  模板被潜在的字符类型(char 或 wchar_t)参数化,因此,对于 TCHAR 版本,所要做的就是使用 TCHAR 来模仿定义。

typedef basic_string< TCHAR,
char_traits< TCHAR >,
allocator< TCHAR > >
tstring;

  现在便有了一个 tstring,它基于 TCHAR——也就是说,它要么是 char,要么是 wchar_t,这取决于 _UNICODE 的值。 以上示范并指出了 STL 是怎样使用 basic_string 来实现基于任何类型的字符串的。定义一个新的 typedef 并不是解决此问题最有效的方法。一个更好的方法是基于 string 和wstring 来简单 地定义 tstring,如下:

#ifdef _UNICODE
#define tstring wstring
#else
#define tstring string
#endif

  这个方法之所以更好,是因为 STL 中已经定义了 string 和 wstring,那为什么还要使用模板来定义一个新的和其中之一一样的字符串类呢? 暂且叫它 tstring。可以用 #define 将 tstring 定义为 string 和 wstring,这样可以避免创建另外一个模板类( 虽然当今的编译器非常智能,如果它把该副本类丢弃,我一点也不奇怪)。[编辑更新-2004/07/30:typedef 不创建新类,只是为某个类型引入限定范围的名称,typedef 决不会定义一个新的类型]。不管怎样,一旦定义了 tstring,便可以像下面这样编码:

tstring s = _T("Hello, world");
_tprintf(_T("s =%s\n"), s.c_str());

  basic_string::c_str 方法返回一个指向潜在字符类型的常量指针;在这里,该字符类型要么是const char*,要么是 const wchar_t*。
  Figure 2 是一个简单的示范程序,举例说明了 tstring 的用法。它将“Hello,world”写入一个文件,并报告写了多少个字节。我对 工程进行了设置,以便用 Unicode 生成 debug 版本,用 MBCS 生成 Release 版本。你可以分别进行编译/生成并运行程序,然后比较结果。Figure 3 显示了例子的运行情况。


Figure 3 运行中的 tstring

  顺便说一下,MFC 和 ATL 现在已经联姻,以便都使用相同的字符串实现。结合后的实现使用一个叫做 CStringT 的模板类,这在某种意义上 ,其机制类似 STL 的 basic_string,用它可以根据任何潜在的字符类型来创建 CString 类。在 MFC 包含文件 afxstr.h 中定义了三种字符 串类型,如下:

typedef ATL::CStringT< wchar_t,
StrTraitMFC< wchar_t > > CStringW;
typedef ATL::CStringT< char,
StrTraitMFC< char > > CStringA;
typedef ATL::CStringT< TCHAR,
StrTraitMFC< TCHAR > > CString;

CStringW,CStringA 和 CString 正是你所期望的:CString 的宽字符,ASCII 和 TCHAR 版本。
  那么,哪一个更好,STL 还是 CStirng?两者都很好,你可以选择你最喜欢的一个。但有一个问题要考虑到:就是你想要链接哪个库,以及你是否已经在使用 MFC/ATL。从编码 的角度来看,我更喜欢 CString 的两个特性:
  其一是无论使用宽字符还是char,都可以很方便地对 CString 进行初始化。

CString s1 = "foo";
CString s2 = _T("bar");

  这两个初始化都正常工作,因为 CString 自己进行了所有必要的转换。使用 STL 字符串,你必须使用_T()对 tstring 进行初始化,因为你 无法通过一个char*初始化一个wstring,反之亦然。
  其二是 CString 对 LPCTSTR 的自动转换操作,你可以像下面这样编码:

CString s;
LPCTSTR lpsz = s;

  另一方面,使用 STL 必须显式调用 c_str 来完成这种转换。这确实有点挑剔,某些人会争辩说,这样能更好地了解何时进行转换。比如, 在C风格可变参数的函数中使用 CString 可能会有麻烦,像 printf:

printf("s=%s\n", s); // 错误
printf("s=%s\n", (LPCTSTR)s); // 必需的

  没有强制类型转换的话,得到的是一些垃圾结果,因为 printf 希望 s 是 char*。我敢肯定很多读者都犯过这种错误。防止这种灾祸是 STL 设计者不提供转换操作符的一个毋庸置疑的理由。而是坚持要你调用 c_str。一般来讲,喜欢使用 STL 家伙趋向于理论和学究气,而 Redmontonians(译者:指微软)的大佬们则更注重实用和散漫。嘿,不管怎样,std::string 和 CString 之间的实用差别是微不足道的。

我正在试图用托管扩展和互用性(interop)向 C# 和 .Net 框架暴露我的 C++ 库。我的一个类中含有一个联合(union)类型,但 .Net 似乎并不支持这种类型:

class MyClass
{
union
{
int i;
double d;
};
};

  使用联合旨在节约空间,因为我知道int和double是绝对不可能同时使用的。同时,我的很多代码都引用了此联合类型,而且我不想更改它们。请问我怎样把这个类暴露给.Net呢?我是不是必须把联合中的每个值分别定义为成员变量,或者使用一个成员方法?
John Bunt
你可以使用这些方法中的任意一个,但不一定非要这么做。在 .Net 的互用性机制中,总有 办法来很好地暴露C++对象——这么说吧,几乎总有办法。公共语言运行时(CLR)不能理解联合类型,但可以用某些特殊技巧的常规 __value struct 来告诉 它成员在哪里。这个神奇的属性就是 StructLayout 和FieldOffset。在托管 C++ 中,它看起来象这样:

[StructLayout(LayoutKind::Explicit)]
public __value struct MyUnion {
[FieldOffset(0)] int i;
[FieldOffset(0)] double d;
};

  这段代码告诉 CLR,整数i和浮点数d均处于结构的零偏移处(也就是说是第一项),这样便使它们交迭,其效果就是把一个结构变成了联合。这样便可以在 __gc 类中 使用 MyUnion,像这样:

public __gc class CUnionClass {
public:
// 可以直接存取,因为它是 public 类型
MyUnion uval;
};

  有了 CUnionClass 的定义,便可以在任何 .Net 语言中通过 uval 直接存取成员i和d。在C#中,它看起来像下面这样:

CUnionClass obj = new CUnionClass();
obj.uval.i = 17;
obj.uval.d = 3.14159

  我写了一个名为 MCUnion 的小程序,它实现了一个托管C++库,它包含前面所示的 CUnionClass,还有一个用于测试这个C++库的 C# 程序 utest(参见 Figure 4和 Figure 5)。CUnionClass 示范了如何为联合成员添加属性,这样你就可以通过 obj.i 和 obj.d,而不是 obj.uval.i 和 obj.uval.d 来 存取值。依照你的设计,这可能是,也可能不是你所想要的结果。如果你愿意,你可以将 uval 设置为 private 或者protected 类型,这样客户端就必须使用属性。这将完全隐藏 uval 的联合 实质特性。测试程序通过联合本身和属性 i 和 d 两种方式都可以存取 i 和 d。

我正在写一个 DirectX 屏保,需要在用户进行屏保设置之前,将从用户 My Pictures 目录下获得JPG,BMP,GIF 和 TGA 文件列表 作为一种默认设置并自行加载它们。将图像纹理设置到 DirectX 中没有什么问题,但我有点担心的就是不同的用户其 My Pictures 目录可能会不 同。在我的机器上,这个路径为“C:\Documents and Settings\Administrator\My Documents\My Pictures”。有没有一个简单的方法获得 My Pictures 的位置呢?

是的,有一个简单的方法。你需要的函数是 SHGetSpecialFolderPath,它可以通过一个预定义的ID,如 CSIDL_MYPICTURES 来找到 对应的专用文件夹,该函数被定义在 ShlObj.h中,其中还定义了很多其它的外壳元素。比如:

TCHAR buf[MAX_PATH];
SHGetSpecialFolderPath(NULL, // HWND
buf,
CSIDL_MYPICTURES,
NULL); // don''t create


  应该总是使用 SHGetSpecialFolderPath 获得专用文件夹的名称(而不是直接搜寻注册表),因为它保证可以在所有版本的 Windows 系统中工作, 包括未来的版本,即便微软的大佬们修改存储专用文件夹路径的注册表键值。对于 Windows 2000 和 Windows XP来说,SHGetSpecialFolderPath 在shell32.dll中。而 Windows 9x 和 Windows NT 等较旧版本不含 SHGetSpecialFolderPath,但Microsoft 通过一个专门的 DLL 提供——SHFOLDER.DLL,你可以 随自己的应用程序免费分发这个DLL文件。
  事实上,来自 Redmond 的官方文档如是说:“鼓励软件供应商尽可能多地重新分发 SHFOLDER.DLL 文件,以便支持 Windows 2000 以前 各个版本。”唯一需要注意的是:如果你的应用程序是面向旧版本的 Windows 操作系统,但是在 Windows 2000 或 Windows XP上生成的, 那么你必须显式的链接 SHFOLDER.DLL;否则链接器将从 Shell32.dll 中得到 SHGetSpecialFolderPath。
  既然这是一个 C++ 专栏,所以我写了一个叫做 CSpecialFolder 的小类(参见 Figure 6),它从 CString 派生,并会自动调用 SHGetSpecialFolderPath。 其使用方法如下:

CSpecialFolder mypics(CSIDL_MYPICTURES);
LPCTSTR lpszPath = mypics;


  这样赋值是行得通的,因为 CSpecialFolder 从 CString 派生而来,它含有一个隐式的到 LPCTSTR 的转换操作符。CSpecialFolder 可以 从下载包中得到,附带有一个测试程序,它可以显示所有在 ShlObj.h 文件中有 CSIDL_XXX 定义的专用文件夹路径名称。其中包含大家熟悉的文件夹,如 :Favorites(收藏夹),Fonts(字体),Programs(程序),History(历史),AppData(应用程序数据)——以及一些 奇怪的文件夹,比如:CSIDL_BITBUCKET(回收站),CSIDL_INTERNET(我想是指 Microsoft IE图标的路径),还有 CSIDL_SYSTEMX86(在 RISC/Alpha For Windows 2000 上,x86 的系统目录)。

向 Paul 提问和评论请发到 cppqa@microsoft.com.
 
作者简介
  Paul DiLascia 是一名自由作家,顾问和 Web/UI 设计者。他是《Writing Reusable Windows Code in C++》书(Addison-Wesley, 1992)的作者。通过 http://www.dilascia.com 可以获得更多了解。  
本文出自 MSDN Magazine 的 August 2004 期刊,可通过当地报摊获得,或者最好是 订阅

本文由 VCKBASE MTT 翻译

c中什么叫转义符_百度知道

转义字符是C语言中表示字符的一种特殊形式。通常使用转义字符表示ASCII码字符集中不可打印的控制字符和特定功能的字符,如用于表示字符常量的单撇号( '),用于表示字符串常量的双撇号( ")和反斜杠( \)等。转义字符用反斜杠\后面跟一个字符或一个八进制或十六进制数表示。

转义字符 意义 ASCII码值(十进制)
\a 响铃(BEL) 007
\b 退格(BS) 008
\f 换页(FF) 012
\n 换行(LF) 010
\r 回车(CR) 013
\t 水平制表(HT) 009
\v 垂直制表(VT) 011
\\ 反斜杠 092
\? 问号字符 063
\' 单引号字符 039
\" 双引号字符 034
\0 空字符(NULL) 000
\ddd 任意字符 三位八进制
\xhh 任意字符 二位十六进制


字符常量中使用单引号和反斜杠以及字符常量中使用双引号和反斜杠时,都必须使用转义字符表示,即在这些字符前加上反斜杠。
在C程序中使用转义字符\ d d d或者\ x h h可以方便灵活地表示任意字符。\ d d d为斜杠后面跟三位八进制数,该三位八进制数的值即为对应的八进制A S C I I码值。\ x后面跟两位十六进制数,该两位十六进制数为对应字符的十六进制A S C I I码值。

使用转义字符时需要注意以下问题:
1) 转义字符中只能使用小写字母,每个转义字符只能看作一个字符。
2) \v 垂直制表和\f 换页符对屏幕没有任何影响,但会影响打印机执行响应操作。
3) 在C程序中,使用不可打印字符时,通常用转义字符表示

[转]VS2005:C++ std::string, std::wstring转换方法 - nid's blog-快乐的日子

[转]VS2005:C++ std::string, std::wstring转换方法 - nid's blog-快乐的日子: "FROM: http://bianyongtao.spaces.live.com/blog/cns!dd6cd3607cce4603!267.entry
FROM: http://bianyongtao.spaces.live.com/blog/cns!dd6cd3607cce4603!267.entry

随着VS2003升级到VS2005,很多以前熟悉的输入输出方式以及参数传递方式都不再有效(参看 vs2003 到vs2005代码升级要点http://bianyongtao.spaces.live.com/blog /cns!DD6CD3607CCE4603!214.entry )。其中根字符串相关的内容是,wcout不再有效,默认参数传递方式由char*改成了wchar_t*等几个方面。为了解决上面的这些问题,这篇文章里,我将给出几种C++ std::string和std::wstring相互转换的转换方法。

第一种方法:调用WideCharToMultiByte()和MultiByteToWideChar(),代码如下(关于详细的解释,可以参考《windows核心编程》):

#include
#include
using namespace std;
//Converting a WChar string to a Ansi string
std::string WChar2Ansi(LPCWSTR pwszSrc)
{
int nLen = WideCharToMultiByte(CP_ACP, 0, pwszSrc, -1, NULL, 0, NULL, NULL);

if (nLen<= 0) return std::string("");

char* pszDst = new char[nLen];
if (NULL == pszDst) return std::string("");

WideCharToMultiByte(CP_ACP, 0, pwszSrc, -1, pszDst, nLen, NULL, NULL);
pszDst[nLen -1] = 0;

std::string strTemp(pszDst);
delete [] pszDst;

return strTemp;
}

string ws2s(wstring& inputws){ return WChar2Ansi(inputws.c_str()); }

//Converting a Ansi string to WChar string

std::wstring Ansi2WChar(LPCSTR pszSrc, int nLen)

{
int nSize = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)pszSrc, nLen, 0, 0);
if(nSize <= 0) return NULL;

WCHAR *pwszDst = new WCHAR[nSize+1];
if( NULL == pwszDst) return NULL;

MultiByteToWideChar(CP_ACP, 0,(LPCSTR)pszSrc, nLen, pwszDst, nSize);
pwszDst[nSize] = 0;

if( pwszDst[0] == 0xFEFF) // skip Oxfeff
for(int i = 0; i < nSize; i ++)
pwszDst = pwszDst[i+1];

wstring wcharString(pwszDst);
delete pwszDst;

return wcharString;
}

std::wstring s2ws(const string& s){ return Ansi2WChar(s.c_str(),s.size());}


第二种方法:采用ATL封装_bstr_t的过渡:(注,_bstr_是Microsoft Specific的,所以下面代码可以在VS2005通过,无移植性);
#include
#include
using namespace std;
#pragma comment(lib, "comsuppw.lib")

string ws2s(const wstring& ws);
wstring s2ws(const string& s);

string ws2s(const wstring& ws)
{
_bstr_t t = ws.c_str();
char* pchar = (char*)t;
string result = pchar;
return result;
}

wstring s2ws(const string& s)
{
_bstr_t t = s.c_str();
wchar_t* pwchar = (wchar_t*)t;
wstring result = pwchar;
return result;
}

第三种方法:使用CRT库的mbstowcs()函数和wcstombs()函数,平台无关,需设定locale。
#include
#include
using namespace std;
string ws2s(const wstring& ws)
{
string curLocale = setlocale(LC_ALL, NULL); // curLocale = "C";

setlocale(LC_ALL, "chs");

const wchar_t* _Source = ws.c_str();
size_t _Dsize = 2 * ws.size() + 1;
char *_Dest = new char[_Dsize];
memset(_Dest,0,_Dsize);
wcstombs(_Dest,_Source,_Dsize);
string result = _Dest;
delete []_Dest;

setlocale(LC_ALL, curLocale.c_str());

return result;
}

wstring s2ws(const string& s)
{
setlocale(LC_ALL, "chs");

const char* _Source = s.c_str();
size_t _Dsize = s.size() + 1;
wchar_t *_Dest = new wchar_t[_Dsize];
wmemset(_Dest, 0, _Dsize);
mbstowcs(_Dest,_Source,_Dsize);
wstring result = _Dest;
delete []_Dest;

setlocale(LC_ALL, "C");

return result;
}

//第四种方法,标准C++转换方法:(待续)
//第五种方法,在C++中使用C#类库:(待续
其中第四种,我的实现始终存在一些问题。 第五种,我只是知道有这么一种方案,没有时间去详细了解,算是给一些提示吧。

2008年7月29日星期二

详细解说STL string -- STLDetailString

详细解说STL string -- STLDetailString

详细解说STL string

0 前言: string 的角色

C++ 语言是个十分优秀的语言,但优秀并不表示完美。还是有许多人不愿意使用C或者C++,为什么?原因众多,其中之一就是C/C++的文本处理功能太麻烦,用 起来很不方便。以前没有接触过其他语言时,每当别人这么说,我总是不屑一顾,认为他们根本就没有领会C++的精华,或者不太懂C++,现在我接触 perl, php, 和Shell脚本以后,开始理解了以前为什么有人说C++文本处理不方便了。

举例来说,如果文本格式是:用户名 电话号码,文件名name.txt

Tom 23245332

Jenny 22231231
Heny 22183942
Tom 23245332
...
现在我们需要对用户名排序,且只输出不同的姓名。

那么在shell 编程中,可以这样用:

awk '{print $1}' name.txt | sort | uniq 
简单吧?

如果使用C/C++ 就麻烦了,他需要做以下工作:

  1. 先打开文件,检测文件是否打开,如果失败,则退出。
  2. 声明一个足够大得二维字符数组或者一个字符指针数组
  3. 读入一行到字符空间
  4. 然后分析一行的结构,找到空格,存入字符数组中。
  5. 关闭文件
  6. 写一个排序函数,或者使用写一个比较函数,使用qsort排序
  7. 遍历数组,比较是否有相同的,如果有,则要删除,copy...
  8. 输出信息
你可以用C++或者C语言去实现这个流程。如果一个人的主要工作就是处理这种类似的文本(例如做apache的日志统计和分析),你说他会喜欢C/C++么?

当然,有了STL,这些处理会得到很大的简化。我们可以使用 fstream来代替麻烦的fopen fread fclose, 用vector 来代替数组。最重要的是用 string来代替char * 数组,使用sort排序算法来排序,用unique 函数来去重。听起来好像很不错 smile 。看看下面代码(例程1):

#include 

#include
#include
#include
#include
using namespace std;
int main(){
ifstream in("name.txt");
string strtmp;
vector vect;
while(getline(in, strtmp, '\n'))
vect.push_back(strtmp.substr(0, strtmp.find(' ')));
sort(vect.begin(), vect.end());
vector::iterator it=unique(vect.begin(), vect.end());
copy(vect.begin(), it, ostream_iterator(cout, "\n"));
return 0;
}
也还不错吧,至少会比想象得要简单得多!(代码里面没有对错误进行处理,只是为了说明问题,不要效仿).

当然,在这个文本格式中,不用vector而使用map会更有扩充性,例如,还可通过人名找电话号码等等,但是使用了map就不那么好用sort了。你可以用map试一试。

这里string的作用不只是可以存储字符串,还可以提供字符串的比较,查找等。在sort和unique函数中就默认使用了less 和equal_to函数, 上面的一段代码,其实使用了string的以下功能:

  1. 存储功能,在getline() 函数中
  2. 查找功能,在find() 函数中
  3. 子串功能,在substr() 函数中
  4. string operator < , 默认在sort() 函数中调用
  5. string operator == , 默认在unique() 函数中调用

总之,有了string 后,C++的字符文本处理功能总算得到了一定补充,加上配合STL其他容器使用,其在文本处理上的功能已经与perl, shell, php的距离缩小很多了。 因此掌握string 会让你的工作事半功倍。

1 string 使用

其实,string并不是一个单独的容器,只是basic_string 模板类的一个typedef 而已,相对应的还有wstring, 你在string 头文件中你会发现下面的代码:
extern "C++" {

typedef basic_string <char> string;
typedef basic_string wstring;
} // extern "C++"
由于只是解释string的用法,如果没有特殊的说明,本文并不区分string 和 basic_string的区别。

string 其实相当于一个保存字符的序列容器,因此除了有字符串的一些常用操作以外,还有包含了所有的序列容器的操作。字符串的常用操作包括:增加、删除、修改、查找比较、链接、输入、输出等。详细函数列表参看附录。不要害怕这么多函数,其实有许多是序列容器带有的,平时不一定用的上。

如果你要想了解所有函数的详细用法,你需要查看basic_string,或者下载STL编程手册。这里通过实例介绍一些常用函数。

1.1 充分使用string 操作符

string 重载了许多操作符,包括 +, +=, <, =, , [], <<, >>等,正式这些操作符,对字符串操作非常方便。先看看下面这个例子:tt.cpp(例程2)
#include 

#include
using namespace std;
int main(){
string strinfo="Please input your name:";
cout << strinfo ;
cin >> strinfo;
if( strinfo == "winter" )
cout << "you are winter!"< else if( strinfo != "wende" )
cout << "you are not wende!"< else if( strinfo < "winter")
cout << "your name should be ahead of winter"< else
cout << "your name should be after of winter"< strinfo += " , Welcome to China!";
cout << strinfo< cout <<"Your name is :"< string strtmp = "How are you? " + strinfo;
for(int i = 0 ; i < strtmp.size(); i ++)
cout< return 0;
}

下面是程序的输出

-bash-2.05b$ make tt

c++ -O -pipe -march=pentiumpro tt.cpp -o tt
-bash-2.05b$ ./tt
Please input your name:Hero
you are not wende!
Hero , Welcome to China!
How are you? Hero , Welcome to China!

有了这些操作符,在STL中仿函数都可以直接使用string作为参数,例如 less, great, equal_to 等,因此在把string作为参数传递的时候,它的使用和int 或者float等已经没有什么区别了。例如,你可以使用:

mapint> mymap;

//以上默认使用了 less
有了 operator + 以后,你可以直接连加,例如:
string strinfo="Winter";

string strlast="Hello " + strinfo + "!";
//你还可以这样:
string strtest="Hello " + strinfo + " Welcome" + " to China" + " !";
看见其中的特点了吗?只要你的等式里面有一个 string 对象,你就可以一直连续"+",但有一点需要保证的是,在开始的两项中,必须有一项是 string 对象。其原理很简单:
  1. 系统遇到"+"号,发现有一项是string 对象。
  2. 系统把另一项转化为一个临时 string 对象。
  3. 执行 operator + 操作,返回新的临时string 对象。
  4. 如果又发现"+"号,继续第一步操作。
由于这个等式是由左到右开始检测执行,如果开始两项都是const char* ,程序自己并没有定义两个const char* 的加法,编译的时候肯定就有问题了。

有了操作符以后,assign(), append(), compare(), at()等函数,除非有一些特殊的需求时,一般是用不上。当然at()函数还有一个功能,那就是检查下标是否合法,如果是使用:

string str="winter";

//下面一行有可能会引起程序中断错误
str[100]='!';
//下面会抛出异常:throws: out_of_range
cout<
了解了吗?如果你希望效率高,还是使用[]来访问,如果你希望稳定性好,最好使用at()来访问。

1.2 眼花缭乱的string find 函数

由于查找是使用最为频繁的功能之一,string 提供了非常丰富的查找函数。其列表如下:
函数名 描述
find 查找
rfind 反向查找
find_first_of 查找包含子串中的任何字符,返回第一个位置
find_first_not_of 查找不包含子串中的任何字符,返回第一个位置
find_last_of 查找包含子串中的任何字符,返回最后一个位置
find_last_not_of 查找不包含子串中的任何字符,返回最后一个位置
以上函数都是被重载了4次,以下是以find_first_of 函数为例说明他们的参数,其他函数和其参数一样,也就是说总共有24个函数 smile
size_type find_first_of(const basic_string& s, size_type pos = 0)

size_type find_first_of(const charT* s, size_type pos, size_type n)
size_type find_first_of(const charT* s, size_type pos = 0)
size_type find_first_of(charT c, size_type pos = 0)
所 有的查找函数都返回一个size_type类型,这个返回值一般都是所找到字符串的位置,如果没有找到,则返回string::npos。有一点需要特别 注意,所有和string::npos的比较一定要用string::size_type来使用,不要直接使用int 或者unsigned int等类型。其实string::npos表示的是-1, 看看头文件:
template <class _CharT, class _Traits, class _Alloc>

const basic_string<_chart,_traits,_alloc>::size_type
basic_string<_chart,_traits,_alloc>::npos
= basic_string<_chart,_traits,_alloc>::size_type) -1;

find 和 rfind 都还比较容易理解,一个是正向匹配,一个是逆向匹配,后面的参数pos都是用来指定起始查找位置。对于find_first_of 和find_last_of 就不是那么好理解。

find_first_of 是给定一个要查找的字符集,找到这个字符集中任何一个字符所在字符串中第一个位置。或许看一个例子更容易明白。

有这样一个需求:过滤一行开头和结尾的所有非英文字符。看看用string 如何实现:

#include 

#include
using namespace std;
int main(){
string strinfo=" //*---Hello Word!......------";
string strset="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int first = strinfo.find_first_of(strset);
if(first == string::npos) {
cout<<"not find any characters"< return -1;
}
int last = strinfo.find_last_of(strset);
if(last == string::npos) {
cout<<"not find any characters"< return -1;
}
cout << strinfo.substr(first, last - first + 1)< return 0;
}
这里把所有的英文字母大小写作为了需要查找的字符集,先查找第一个英文字母的位置,然后查找最后一个英文字母的位置,然后用substr 来的到中间的一部分,用于输出结果。下面就是其结果:
Hello Word
前面的符号和后面的符号都没有了。像这种用法可以用来查找分隔符,从而把一个连续的字符串分割成为几部分,达到 shell 命令中的 awk 的用法。特别是当分隔符有多个的时候,可以一次指定。例如有这样的需求:
张三|3456123, 湖南

李四,4564234| 湖北
王小二, 4433253|北京
...
我们需要以 "|" ","为分隔符,同时又要过滤空格,把每行分成相应的字段。可以作为你的一个家庭作业来试试,要求代码简洁。

1.3 string insert, replace, erase

了解了string 的操作符,查找函数和substr,其实就已经了解了string的80%的操作了。insert函数, replace函数和erase函数在使用起来相对简单。下面以一个例子来说明其应用。

string只是提供了按照位置和区间的replace函数,而不能用一个string字串来替换指定string中的另一个字串。这里写一个函数来实现这个功能:

void string_replace(string & strBig, const string & strsrc, const string &strdst) {

string::size_type pos=0;
string::size_type srclen=strsrc.size();
string::size_type dstlen=strdst.size();
while( (pos=strBig.find(strsrc, pos)) != string::npos){
strBig.replace(pos, srclen, strdst);
pos += dstlen;
}
}
看看如何调用:
#include 

#include
using namespace std;
int main() {
string strinfo="This is Winter, Winter is a programmer. Do you know Winter?";
cout<<"Orign string is :\n"< string_replace(strinfo, "Winter", "wende");
cout<<"After replace Winter with wende, the string is :\n"< return 0;
}
其输出结果:
Orign string is :

This is Winter, Winter is a programmer. Do you know Winter?
After replace Winter with wende, the string is :
This is wende, wende is a programmer. Do you know wende?
如果不用replace函数,则可以使用erase和insert来替换,也能实现string_replace函数的功能:
void string_replace(string & strBig, const string & strsrc, const string &strdst) {

string::size_type pos=0;
string::size_type srclen=strsrc.size();
string::size_type dstlen=strdst.size();
while( (pos=strBig.find(strsrc, pos)) != string::npos){
strBig.erase(pos, srclen);
strBig.insert(pos, strdst);
pos += dstlen;
}
}
当然,这种方法没有使用replace来得直接。

2 string 和 C风格字符串

现在看了这么多例子,发现const char* 可以和string 直接转换,例如我们在上面的例子中,使用
string_replace(strinfo, "Winter", "wende");
来代用
void string_replace(string & strBig, const string & strsrc, const string &strdst) 
在C语言中只有char* 和 const char*,为了使用起来方便,string提供了三个函数满足其要求:
const charT* c_str() const

const charT* data() const
size_type copy(charT* buf, size_type n, size_type pos = 0) const
其中:
  1. c_str 直接返回一个以\0结尾的字符串。
  2. data 直接以数组方式返回string的内容,其大小为size()的返回值,结尾并没有\0字符。
  3. copy 把string的内容拷贝到buf空间中。
你或许会问,c_str()的功能包含data(),那还需要data()函数干什么?看看源码:
const charT* c_str () const

{ if (length () == 0) return ""; terminate (); return data (); }
原 来c_str()的流程是:先调用terminate(),然后在返回data()。因此如果你对效率要求比较高,而且你的处理又不一定需要以\0的方式 结束,你最好选择data()。但是对于一般的C函数中,需要以const char*为输入参数,你就要使用c_str()函数。

对于c_str() data()函数,返回的数组都是由string本身拥有,千万不可修改其内容。其原因是许多string实现的时候采用了引用机制,也就是说,有可能几 个string使用同一个字符存储空间。而且你不能使用sizeof(string)来查看其大小。详细的解释和实现查看Effective STL的条款15:小心string实现的多样性

另外在你的程序中,只在需要时才使用c_str()或者data()得到字符串,每调用一次,下次再使用就会失效,如:

string strinfo("this is Winter");

...
//最好的方式是:
foo(strinfo.c_str());
//也可以这么用:
const char* pstr=strinfo.c_str();
foo(pstr);
//不要再使用了pstr了, 下面的操作已经使pstr无效了。
strinfo += " Hello!";
foo(pstr);//错误!
会遇到什么错误?当你幸运的时候pstr可能只是指向"this is Winter Hello!"的字符串,如果不幸运,就会导致程序出现其他问题,总会有一些不可遇见的错误。总之不会是你预期的那个结果。

3 string 和 Charactor Traits

了解了string的用法,该详细看看string的真相了。前面提到string 只是basic_string的一个typedef。看看basic_string 的参数:
template <class charT, class traits = char_traits,

class Allocator = allocator >
class basic_string
{
//...
}
char_traits不仅是在basic_string 中有用,在basic_istream 和 basic_ostream中也需要用到。

就像Steve Donovan在过度使用C++模板中提到的,这些确实有些过头了,要不是系统自己定义了相关的一些属性,而且用了个typedef,否则还真不知道如何使用。

但复杂总有复杂道理。有了char_traits,你可以定义自己的字符串类型。当然,有了char_traits <> 和char_traits <> 你的需求使用已经足够了,为了更好的理解string ,咱们来看看char_traits都有哪些要求。

如果你希望使用你自己定义的字符,你必须定义包含下列成员的结构:

表达式 描述
char_type 字符类型
int_type int 类型
pos_type 位置类型
off_type 表示位置之间距离的类型
state_type 表示状态的类型
assign(c1,c2) 把字符c2赋值给c1
eq(c1,c2) 判断c1,c2 是否相等
lt(c1,c2) 判断c1是否小于c2
length(str) 判断str的长度
compare(s1,s2,n) 比较s1和s2的前n个字符
copy(s1,s2, n) 把s2的前n个字符拷贝到s1中
move(s1,s2, n) 把s2中的前n个字符移动到s1中
assign(s,n,c) 把s中的前n个字符赋值为c
find(s,n,c) 在s的前n个字符内查找c
eof() 返回end-of-file
to_int_type(c) 将c转换成int_type
to_char_type(i) 将i转换成char_type
not_eof(i) 判断i是否为EOF
eq_int_type(i1,i2) 判断i1和i2是否相等
想看看实际的例子,你可以看看sgi STL的char_traits结构源码.

现在默认的string版本中,并不支持忽略大小写的比较函数和查找函数,如果你想练练手,你可以试试改写一个char_traits , 然后生成一个case_string类, 也可以在string 上做继承,然后派生一个新的类,例如:ext_string,提供一些常用的功能,例如:

  1. 定义分隔符。给定分隔符,把string分为几个字段。
  2. 提供替换功能。例如,用winter, 替换字符串中的wende
  3. 大小写处理。例如,忽略大小写比较,转换等
  4. 整形转换。例如把"123"字符串转换为123数字。
这些都是常用的功能,如果你有兴趣可以试试。其实有人已经实现了,看看Extended STL string。如果你想偷懒,下载一个头文件就可以用,有了它确实方便了很多。要是有人能提供一个支持正则表达式的string,我会非常乐意用。

4 string 建议

使用string 的方便性就不用再说了,这里要重点强调的是string的安全性。
  1. string并不是万能的,如果你在一个大工程中需要频繁处理字符串,而且有可能是多线程,那么你一定要慎重(当然,在多线程下你使用任何STL容器都要慎重)。
  2. string的实现和效率并不一定是你想象的那样,如果你对大量的字符串操作,而且特别关心其效率,那么你有两个选择,首先,你可以看看你使用的STL版本中string实现的源码;另一选择是你自己写一个只提供你需要的功能的类。
  3. string的c_str()函数是用来得到C语言风格的字符串,其返回的指针不能修改其空间。而且在下一次使用时重新调用获得新的指针。
  4. string的data()函数返回的字符串指针不会以'\0'结束,千万不可忽视。
  5. 尽量去使用操作符,这样可以让程序更加易懂(特别是那些脚本程序员也可以看懂)

5 小结

难怪有人说:
string 使用方便功能强,我们一直用它!

6 附录

string 函数列表
函数名 描述
begin 得到指向字符串开头的Iterator
end 得到指向字符串结尾的Iterator
rbegin 得到指向反向字符串开头的Iterator
rend 得到指向反向字符串结尾的Iterator
size 得到字符串的大小
length 和size函数功能相同
max_size 字符串可能的最大大小
capacity 在不重新分配内存的情况下,字符串可能的大小
empty 判断是否为空
operator[] 取第几个元素,相当于数组
c_str 取得C风格的const char* 字符串
data 取得字符串内容地址
operator= 赋值操作符
reserve 预留空间
swap 交换函数
insert 插入字符
append 追加字符
push_back 追加字符
operator+= += 操作符
erase 删除字符串
clear 清空字符容器中所有内容
resize 重新分配空间
assign 和赋值操作符一样
replace 替代
copy 字符串到空间
find 查找
rfind 反向查找
find_first_of 查找包含子串中的任何字符,返回第一个位置
find_first_not_of 查找不包含子串中的任何字符,返回第一个位置
find_last_of 查找包含子串中的任何字符,返回最后一个位置
find_last_not_of 查找不包含子串中的任何字符,返回最后一个位置
substr 得到字串
compare 比较字符串
operator+ 字符串链接
operator== 判断是否相等
operator!= 判断是否不等于
operator< 判断是否小于
operator>> 从输入流中读入字符串
operator<< 字符串写入输出流
getline 从输入流中读入一行

7 参考文章

  1. SGI STL: char_traits 源码
  2. STL 编程手册: basic_string
  3. 详细解说 STL 排序(Sort)
  4. 详细解说 STL hash_map系列
  5. Effective STL 中文版

论坛讨论讨论:详细解说STL string


Windows系统编程之进程间通信

Windows系统编程之进程间通信
Windows系统编程之进程间通信
作者:北极星2003
来源:看雪论坛(www.pediy.com)

附件:windowipc.rar

Windows 的IPC(进程间通信)机制主要是异步管道和命名管道。(至于其他的IPC方式,例如内存映射、邮槽等这里就不介绍了)
管道(pipe)是用于进程间通信的共享内存区域。创建管道的进程称为管道服务器,而连接到这个管道的进程称为管道客户端。一个进程向管道写入信息,而另外一个进程从管道读取信息。
异步管道是基于字符和半双工的(即单向),一般用于程序输入输出的重定向;命名管道则强大地多,它们是面向消息和全双工的,同时还允许网络通信,用于创建客户端/服务器系统。
一、异步管道(实现比较简单,直接通过实例来讲解)
实验目标:当前有sample.cpp, sample.exe, sample.in这三个文件,sample.exe为sample.cpp的执行程序,sample.cpp只是一个简单的程序示例(简单求和),如下:

代码:
#include
int main()
{
int a, b ;
while ( cin >> a >> b && ( a || b ) )
cout <<>sample.out”,这个命令也是利用管道特性实现的,现在我们就根据异步管道的实现原理自己来实现这个功能。
管道是基于半双工(单向)的,这里有两个重定向的过程,显然需要创建两个管道,下面给出流程图:

异步管道实现的流程图说明:
1)。父进程是我们需要实现的,其中需要创建管道A,管道B,和子进程,整个实现流程分为4个操作。
2)。管道A:输入管道
3)。管道B:输出管道
4)。操作A:把输入文件sample.in的数据写入输入管道(管道A)
5)。操作B:子进程从输入管道中读取数据,作为该进程的加工原料。通常,程序的输入数据由标准的输入设备输入,这里实现输入重定向,即把输入管道作为输入设备。
6)。操作C:子进程把加工后的成品(输出数据)输出到输出管道。通常,程序的输出数据会输出到标准的输出设备,一般为屏幕,这里实现输出重定向,即把输出管道作为输出设备。
7)。操作D:把输出管道的数据写入输出文件
需要注意的是,管道的本质只是一个共享的内存区域。这个实验中,管道区域处于父进程的地址空间中,父进程的作用是提供环境和资源,并协调子进程进行加工。
程序源码:

代码:
#include
#include

const int BUFSIZE = 4096 ;
HANDLE hChildStdinRd, hChildStdinWr, hChildStdinWrDup,
hChildStdoutRd,hChildStdoutWr,hChildStdoutRdDup,
hSaveStdin, hSaveStdout;

BOOL CreateChildProcess(LPTSTR);
VOID WriteToPipe(LPTSTR);
VOID ReadFromPipe(LPTSTR);
VOID ErrorExit(LPTSTR);
VOID ErrMsg(LPTSTR, BOOL);
void main( int argc, char *argv[] )
{
// 处理输入参数
if ( argc != 4 )
return ;

// 分别用来保存命令行,输入文件名(CPP/C),输出文件名(保存编译信息)
LPTSTR lpProgram = new char[ strlen(argv[1]) ] ;
strcpy ( lpProgram, argv[1] ) ;
LPTSTR lpInputFile = new char[ strlen(argv[2]) ];
strcpy ( lpInputFile, argv[2] ) ;
LPTSTR lpOutputFile = new char[ strlen(argv[3]) ] ;
strcpy ( lpOutputFile, argv[3] ) ;

SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;

/************************************************
* redirecting child process's STDOUT *
************************************************/
hSaveStdout = GetStdHandle(STD_OUTPUT_HANDLE);

if (! CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0))
ErrorExit("Stdout pipe creation failed\n");

if (! SetStdHandle(STD_OUTPUT_HANDLE, hChildStdoutWr))
ErrorExit("Redirecting STDOUT failed");

BOOL fSuccess = DuplicateHandle(
GetCurrentProcess(),
hChildStdoutRd,
GetCurrentProcess(),
&hChildStdoutRdDup ,
0,
FALSE,
DUPLICATE_SAME_ACCESS);
if( !fSuccess )
ErrorExit("DuplicateHandle failed");
CloseHandle(hChildStdoutRd);

/************************************************
* redirecting child process's STDIN *
************************************************/
hSaveStdin = GetStdHandle(STD_INPUT_HANDLE);

if (! CreatePipe(&hChildStdinRd, &hChildStdinWr, &saAttr, 0))
ErrorExit("Stdin pipe creation failed\n");

if (! SetStdHandle(STD_INPUT_HANDLE, hChildStdinRd))
ErrorExit("Redirecting Stdin failed");

fSuccess = DuplicateHandle(
GetCurrentProcess(),
hChildStdinWr,
GetCurrentProcess(),
&hChildStdinWrDup,
0,
FALSE,
DUPLICATE_SAME_ACCESS);
if (! fSuccess)
ErrorExit("DuplicateHandle failed");
CloseHandle(hChildStdinWr);

/************************************************
* 创建子进程(即启动SAMPLE.EXE) *
************************************************/
fSuccess = CreateChildProcess( lpProgram );
if ( !fSuccess )
ErrorExit("Create process failed");

// 父进程输入输出流的还原设置
if (! SetStdHandle(STD_INPUT_HANDLE, hSaveStdin))
ErrorExit("Re-redirecting Stdin failed\n");
if (! SetStdHandle(STD_OUTPUT_HANDLE, hSaveStdout))
ErrorExit("Re-redirecting Stdout failed\n");

WriteToPipe( lpInputFile ) ;
ReadFromPipe( lpOutputFile );
delete lpProgram ;
delete lpInputFile ;
delete lpOutputFile ;
}

BOOL CreateChildProcess( LPTSTR lpProgram )
{
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
BOOL bFuncRetn = FALSE;

ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) );
ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
siStartInfo.cb = sizeof(STARTUPINFO);

bFuncRetn = CreateProcess ( NULL, lpProgram, NULL, NULL, TRUE, \
0, NULL, NULL, &siStartInfo, &piProcInfo);
if (bFuncRetn == 0)
{
ErrorExit("CreateProcess failed\n");
return 0;
}
else
{
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
return bFuncRetn;
}
}

VOID WriteToPipe( LPTSTR lpInputFile )
{
HANDLE hInputFile = CreateFile(lpInputFile, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
if (hInputFile == INVALID_HANDLE_VALUE)
return ;

BOOL fSuccess ;
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE] = {0} ;

for (;;)
{
fSuccess = ReadFile( hInputFile, chBuf, BUFSIZE, &dwRead, NULL) ;
if ( !fSuccess || dwRead == 0)
break;

fSuccess = WriteFile( hChildStdinWrDup, chBuf, dwRead, &dwWritten, NULL) ;
if ( !fSuccess )
break;
}

if (! CloseHandle(hChildStdinWrDup))
ErrorExit("Close pipe failed\n");

CloseHandle ( hInputFile ) ;
}

VOID ReadFromPipe( LPTSTR lpOutputFile )
{
HANDLE hOutputFile = CreateFile( lpOutputFile, GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hOutputFile == INVALID_HANDLE_VALUE)
return ;

BOOL fSuccess ;
DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE] = { 0 };

if (!CloseHandle(hChildStdoutWr))
ErrorExit("Closing handle failed");

for (;;)
{
fSuccess = ReadFile( hChildStdoutRdDup, chBuf, BUFSIZE, &dwRead, NULL) ;
if( !fSuccess || dwRead == 0)
{
break;
}
fSuccess = WriteFile( hOutputFile, chBuf, dwRead, &dwWritten, NULL) ;
if ( !fSuccess )
break;
}

CloseHandle ( hOutputFile ) ;
}
VOID ErrorExit (LPTSTR lpszMessage)
{
MessageBox( 0, lpszMessage, 0, 0 );
}


二、命名管道
命名管道具有以下几个特征:
(1)命名管道是双向的,所以两个进程可以通过同一管道进行交互。
(2)命名管道不但可以面向字节流,还可以面向消息,所以读取进程可以读取写进程发送的不同长度的消息。
(3)多个独立的管道实例可以用一个名称来命名。例如几个客户端可以使用名称相同的管道与同一个服务器进行并发通信。
(4)命名管道可以用于网络间两个进程的通信,而其实现的过程与本地进程通信完全一致。
实验目标:在客户端输入数据a和b,然后发送到服务器并计算a+b,然后把计算结果发送到客户端。可以多个客户端与同一个服务器并行通信。
界面设计:

难点所在:
实现的过程比较简单,但有一个难点。原本当服务端使用ConnectNamedPipe函数后,如果有客户端连接,就可以直接进行交互。原来我在实现过程中,当管道空闲时,管道的线程函数会无限(INFINITE)阻塞。若现在需要停止服务,就必须结束所有的线程,TernimateThread可以作为一个结束线程的方法,但我基本不用这个函数。一旦使用这个函数之后,目标线程就会立即结束,但如果此时的目标线程正在操作互斥资源、内核调用、或者是操作共享DLL的全局变量,可能会出现互斥资源无法释放、内核异常等现象。这里我用重叠I/0来解决这个问题,在创建PIPE时使用 FILE_FLAG_OVERLAPPED标志,这样使用ConnectNamedPipe后会立即返回,但线程的阻塞由等待函数 WaitForSingleObject来实现,等待OVERLAPPED结构的事件对象被设置。
客户端主要代码:

代码:
void CMyDlg::OnSubmit()
{
// 打开管道
HANDLE hPipe = CreateFile("\\\\.\\Pipe\\NamedPipe", GENERIC_READ | GENERIC_WRITE, \
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL) ;
if ( hPipe == INVALID_HANDLE_VALUE )
{
this->MessageBox ( "打开管道失败,服务器尚未启动,或者客户端数量过多" ) ;
return ;
}

DWORD nReadByte, nWriteByte ;
char szBuf[1024] = {0} ;
// 把两个整数(a,b)格式化为字符串
sprintf ( szBuf, "%d %d", this->nFirst, this->nSecond ) ;
// 把数据写入管道
WriteFile ( hPipe, szBuf, strlen(szBuf), &nWriteByte, NULL ) ;

memset ( szBuf, 0, sizeof(szBuf) ) ;
// 读取服务器的反馈信息
ReadFile ( hPipe, szBuf, 1024, &nReadByte, NULL ) ;
// 把返回信息格式化为整数
sscanf ( szBuf, "%d", &(this->nResValue) ) ;
this->UpdateData ( false ) ;
CloseHandle ( hPipe ) ;
}


服务端主要代码:

代码:
// 启动服务
void CMyDlg::OnStart()
{
CString lpPipeName = "\\\\.\\Pipe\\NamedPipe" ;
for ( UINT i = 0; i < hpipe =" CreateNamedPipe" hpipe ="="" dwerrorcode =" GetLastError">MessageBox ( "创建管道错误!" ) ;
return ;
}
// 为每个管道实例创建一个事件对象,用于实现重叠IO
PipeInst[i].hEvent = CreateEvent ( NULL, false, false, false ) ;
// 为每个管道实例分配一个线程,用于响应客户端的请求
PipeInst[i].hTread = AfxBeginThread ( ServerThread, &PipeInst[i], THREAD_PRIORITY_NORMAL ) ;
}

this->SetWindowText ( "命名管道实例之服务器(运行)" ) ;
this->MessageBox ( "服务启动成功" ) ;
}
// 停止服务
void CMyDlg::OnStop()
{
DWORD dwNewMode = PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_NOWAIT ;
for ( UINT i = 0; i <>SetWindowText ( "命名管道实例之服务器" ) ;
this->MessageBox ( "停止启动成功" ) ;
}

// 线程服务函数
UINT ServerThread ( LPVOID lpParameter )
{
DWORD nReadByte = 0, nWriteByte = 0, dwByte = 0 ;
char szBuf[MAX_BUFFER_SIZE] = {0} ;
PIPE_INSTRUCT CurPipeInst = *(PIPE_INSTRUCT*)lpParameter ;
OVERLAPPED OverLapStruct = { 0, 0, 0, 0, CurPipeInst.hEvent } ;
while ( true )
{
memset ( szBuf, 0, sizeof(szBuf) ) ;
// 命名管道的连接函数,等待客户端的连接(只针对NT)
ConnectNamedPipe ( CurPipeInst.hPipe, &OverLapStruct ) ;
// 实现重叠I/0,等待OVERLAPPED结构的事件对象
WaitForSingleObject ( CurPipeInst.hEvent, INFINITE ) ;
// 检测I/0是否已经完成,如果未完成,意味着该事件对象是人工设置,即服务需要停止
if ( !GetOverlappedResult ( CurPipeInst.hPipe, &OverLapStruct, &dwByte, true ) )
break ;

// 从管道中读取客户端的请求信息
if ( !ReadFile ( CurPipeInst.hPipe, szBuf, MAX_BUFFER_SIZE, &nReadByte, NULL ) )
{
MessageBox ( 0, "读取管道错误!", 0, 0 ) ;
break ;
}

int a, b ;
sscanf ( szBuf, "%d %d", &a, &b ) ;
pMyDlg->nFirst = a ;
pMyDlg->nSecond = b ;
pMyDlg->nResValue = a + b ;
memset ( szBuf, 0, sizeof(szBuf) ) ;
sprintf ( szBuf, "%d", pMyDlg->nResValue ) ;
// 把反馈信息写入管道
WriteFile ( CurPipeInst.hPipe, szBuf, strlen(szBuf), &nWriteByte, NULL ) ;
pMyDlg->SetDlgItemInt ( IDC_FIRST, a, true ) ;
pMyDlg->SetDlgItemInt ( IDC_SECOND, b, true ) ;
pMyDlg->SetDlgItemInt ( IDC_RESULT, pMyDlg->nResValue, true ) ;
// 断开客户端的连接,以便等待下一客户的到来
DisconnectNamedPipe ( CurPipeInst.hPipe ) ;
}

return 0 ;
}

windows进程间通讯的各种方法 - Windows - northtree

windows进程间通讯的各种方法 - Windows - northtree: "windows进程间通讯的各种方法

方法一:WM_COPYDATA
HWND hReceiveDataWindow FindWindow NULL,....
COPYDATASTRUCT data;
data.cbdata strlen pStr ;
data.lpData pStr;
SendMessage hReceiveDataWindow ,WM_COPYDATA, WPARAM GetFocus , LPARAM &data ;

REF.最简单的方式
http://www.cppblog.com/TechLab/archive/2005/12/30/2272.aspx


方法二:dll共享
#pragma data_seg .ASHARE
int iWhatYouUseInTwo 0;
#pragma data_seg

方法三:映象文件
CreateFileMapping
REF.最基础,效率最高的方法
最好的参考书《Windows核心编程》第17章 内存映射文件
http://blog.codingnow.com/2005/10/interprocess_communications.html


方法四:匿名管道:CreatePipe

方法五:命名管道:createNAMEdpipe"

windows下进程间通信的手段有哪些? - tanliyoung的专栏 - CSDNBlog

windows下进程间通信的手段有哪些? - tanliyoung的专栏 - CSDNBlog
摘 要 随着人们对应用程序的要求越来越高,单进程应用在许多场合已不能满足人们的要求。编写多进程/多线程程序成为现代程序设计的一个重要特点,在多进程程序设计中,进程间的通信是不可避免的。Microsoft Win32 API提供了多种进程间通信的方法,全面地阐述了这些方法的特点,并加以比较和分析,希望能给读者选择通信方法提供参考。
关键词 进程 进程通信 IPC Win32 API

1 进程与进程通信

  进程是装入内存并准备执行的程序,每个进程都有私有的虚拟地址空间,由代码、数据以及它可利用的系统资源(如文件、管道等)组成。多进程/多线程是Windows操作系统的一个基本特征。Microsoft Win32应用编程接口(Application Programming Interface, API)提供了大量支持应用程序间数据共享和交换的机制,这些机制行使的活动称为进程间通信(InterProcess Communication, IPC),进程通信就是指不同进程间进行数据共享和数据交换。
  正因为使用Win32 API进行进程通信方式有多种,如何选择恰当的通信方式就成为应用开发中的一个重要问题,下面本文将对Win32中进程通信的几种方法加以分析和比较。

2 进程通信方法

2.1 文件映射
  文件映射(Memory-Mapped Files)能使进程把文件内容当作进程地址区间一块内存那样来对待。因此,进程不必使用文件I/O操作,只需简单的指针操作就可读取和修改文件的内容。
  Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,实现了对文件中数据的共享。
  应用程序有三种方法来使多个进程共享一个文件映射对象。
  (1)继承:第一个进程建立文件映射对象,它的子进程继承该对象的句柄。
  (2)命名文件映射:第一个进程在建立文件映射对象时可以给该对象指定一个名字(可与文件名不同)。第二个进程可通过这个名字打开此文件映射对象。另外,第一个进程也可以通过一些其它IPC机制(有名管道、邮件槽等)把名字传给第二个进程。
  (3)句柄复制:第一个进程建立文件映射对象,然后通过其它IPC机制(有名管道、邮件槽等)把对象句柄传递给第二个进程。第二个进程复制该句柄就取得对该文件映射对象的访问权限。
  文件映射是在多个进程间共享数据的非常有效方法,有较好的安全性。但文件映射只能用于本地机器的进程之间,不能用于网络中,而开发者还必须控制进程间的同步。
2.2 共享内存
  Win32 API中共享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
2.3 匿名管道
  管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的一管道的两端点既可读也可写。
  匿名管道(Anonymous Pipe)是在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。
  匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
2.4 命名管道
  命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。
  命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。
2.5 邮件槽
  邮件槽(Mailslots)提供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。
  通过邮件槽可以给本地计算机上的邮件槽、其它计算机上的邮件槽或指定网络区域中所有计算机上有同样名字的邮件槽发送消息。广播通信的消息长度不能超过400字节,非广播消息的长度则受邮件槽服务器指定的最大消息长度的限制。
  邮件槽与命名管道相似,不过它传输数据是通过不可靠的数据报(如TCP/IP协议中的UDP包)完成的,一旦网络发生错误则无法保证消息正确地接收,而命名管道传输数据则是建立在可靠连接基础上的。不过邮件槽有简化的编程接口和给指定网络区域内的所有计算机广播消息的能力,所以邮件槽不失为应用程序发送和接收消息的另一种选择。
2.6 剪贴板
  剪贴板(Clipped Board)实质是Win32 API中一组用来传输数据的函数和消息,为Windows应用程序之间进行数据共享提供了一个中介,Windows已建立的剪切(复制)-粘贴的机制为不同应用程序之间共享不同格式数据提供了一条捷径。当用户在应用程序中执行剪切或复制操作时,应用程序把选取的数据用一种或多种格式放在剪贴板上。然后任何其它应用程序都可以从剪贴板上拾取数据,从给定格式中选择适合自己的格式。
  剪贴板是一个非常松散的交换媒介,可以支持任何数据格式,每一格式由一无符号整数标识,对标准(预定义)剪贴板格式,该值是Win32 API定义的常量;对非标准格式可以使用Register Clipboard Format函数注册为新的剪贴板格式。利用剪贴板进行交换的数据只需在数据格式上一致或都可以转化为某种格式就行。但剪贴板只能在基于Windows的程序中使用,不能在网络上使用。
2.7 动态数据交换
  动态数据交换(DDE)是使用共享内存在应用程序之间进行数据交换的一种进程间通信形式。应用程序可以使用DDE进行一次性数据传输,也可以当出现新数据时,通过发送更新值在应用程序间动态交换数据。
  DDE和剪贴板一样既支持标准数据格式(如文本、位图等),又可以支持自己定义的数据格式。但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎总是用作对用户指定操作的一次性应答-如从菜单中选择Paste命令。尽管DDE也可以由用户启动,但它继续发挥作用一般不必用户进一步干预。DDE有三种数据交换方式:
  (1) 冷链:数据交换是一次性数据传输,与剪贴板相同。
  (2) 温链:当数据交换时服务器通知客户,然后客户必须请求新的数据。
  (3) 热链:当数据交换时服务器自动给客户发送数据。
  DDE交换可以发生在单机或网络中不同计算机的应用程序之间。开发者还可以定义定制的DDE数据格式进行应用程序之间特别目的IPC,它们有更紧密耦合的通信要求。大多数基于Windows的应用程序都支持DDE。
2.8 对象连接与嵌入
  应用程序利用对象连接与嵌入(OLE)技术管理复合文档(由多种数据格式组成的文档),OLE提供使某应用程序更容易调用其它应用程序进行数据编辑的服务。例如,OLE支持的字处理器可以嵌套电子表格,当用户要编辑电子表格时OLE库可自动启动电子表格编辑器。当用户退出电子表格编辑器时,该表格已在原始字处理器文档中得到更新。在这里电子表格编辑器变成了字处理器的扩展,而如果使用DDE,用户要显式地启动电子表格编辑器。
  同DDE技术相同,大多数基于Windows的应用程序都支持OLE技术。
2.9 动态连接库
  Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。
  虽然可以通过DLL进行进程间数据共享,但从数据安全的角度考虑,我们并不提倡这种方法,使用带有访问权限控制的共享内存的方法更好一些。
2.10 远程过程调用
  Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。RPC既可以在单机不同进程间使用也可以在网络中使用。
  由于Win32 API提供的RPC服从OSF-DCE(Open Software Foundation Distributed Computing Environment)标准。所以通过Win32 API编写的RPC应用程序能与其它操作系统上支持DEC的RPC应用程序通信。使用RPC开发者可以建立高性能、紧密耦合的分布式应用程序。
2.11 NetBios函数
  Win32 API提供NetBios函数用于处理低级网络控制,这主要是为IBM NetBios系统编写与Windows的接口。除非那些有特殊低级网络功能要求的应用程序,其它应用程序最好不要使用NetBios函数来进行进程间通信。
2.12 Sockets
  Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网络编程接口。除了Berkeley Socket原有的库函数以外,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机制进行编程。
  现在通过Sockets实现进程通信的网络应用越来越多,这主要的原因是Sockets的跨平台性要比其它IPC机制好得多,另外WinSock 2.0不仅支持TCP/IP协议,而且还支持其它协议(如IPX)。Sockets的唯一缺点是它支持的是底层通信操作,这使得在单机的进程间进行简单数据传递不太方便,这时使用下面将介绍的WM_COPYDATA消息将更合适些。
2.13 WM_COPYDATA消息
  WM_COPYDATA是一种非常强大却鲜为人知的消息。当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,参数是目的窗口的句柄、传递数据的起始地址、WM_COPYDATA消息。接收方只需像处理其它消息那样处理WM_COPY DATA消息,这样收发双方就实现了数据共享。
  WM_COPYDATA是一种非常简单的方法,它在底层实际上是通过文件映射来实现的。它的缺点是灵活性不高,并且它只能用于Windows平台的单机环境下。

3 结束语

  Win32 API为应用程序实现进程间通信提供了如此多种选择方案,那么开发者如何进行选择呢?通常在决定使用哪种IPC方法之前应考虑以下一些问题:
  (1)应用程序是在网络环境下还是在单机环境下工作。

Windows 下的进程间通讯及数据共享

Windows 下有很多方法实现进程间通讯,比如用 socket,管道(Pipe),信箱(Mailslot),等等。但最基本最直接的还是使用内存共享。其他方法最终还是会绕道这里。

可想而知,如果物理内存只有一份,让这份内存在不同的进程中,映射到各自的虚拟地址空间上,每个进程都可以读取同一份数据,是一种最高效的数据交换方法。下面我们就讨论如何实现它。

共享内存在 Windows 中是用 FileMapping 实现的。我们可以用 CreateFileMapping 创建一个内存文件映射对象, CreateFileMapping 这个 API 将创建一个内核对象,用于映射文件到内存。这里,我们并不需要一个实际的文件,所以,就不需要调用 CreateFile 创建一个文件, hFile 这个参数可以填写 INVALID_HANDLE_VALUE 。但是,文件长度是需要填的。Windows 支持长达 64bit 的文件,但是这里,我们的需求一定不会超过 4G , dwMaximumSizeHigh 一定是 0 ,长度填在 dwMaximumSizeLow 即可。然后调用 MapViewOfFile 映射到当前进程的虚拟地址上即可。一旦用完共享内存,再调用 UnmapViewOfFile 回收内存地址空间。

Windows 把 CreateFileMapping 和 MapViewOfFile 两个 API 分开做是有它的道理的。这是因为允许映射一个超过 4G 的文件,而地址空间最大只有 4G (实际上,一般用户的程序只能用到 2G) , MapViewOfFile 就可以指定文件的 Offset 而只映射一部分。

在 CreateFileMapping 的最后一个参数 pszName 填写一个名字,那么别的进程就可以用这个名字去调用 OpenFileMapping 来打开这个 FileMapping 对象,在新的进程内作映射。 不过,通过约定字符串的方法似乎不太优雅。

一个优雅的方法是,用 DuplicateHandle 在新进程中复制一份 FileMapping 对象出来,然后想办法把 Handle 通知新进程,比如用消息的方式传递过去。

如果需要共享内存的两个进程是父子关系,那么我们可以不用消息传递的方式来通知 FileMapping 的 Handle 。父进程可以用继承 Handle 的方式直接把 FileMapping 的 Handle 传递到子进程中。当然,在 CreateFileMapping 时就应该设置可以被继承的属性。

大约是这样:

SECURITY_ATTRIBUTES sa;
sa.nLength=sizeof(sa);
sa.lpSecurityDescriptor=NULL;
sa.bInheritHandle=TRUE;
handle=CreateFileMapping(INVALID_HANDLE_VALUE,&sa,PAGE_READWRITE,0,size,NULL);

这样,在 CreateProcess 的时候,如果 bInheritHandles 参数为 TRUE ,所有有可被继承属性的内核对象都会被复制到子进程中。

注:内核对象的继承就是在 CreateProcess 创建子进程,但是子进程的主线程尚未活动之前,内核扫描当前进程中所有内核对象,检查出有可继承属性的那些,再用 DuplicateHandle 复制一份到子进程。由于是内核对象,在内核中实质只有一份,所有只是引用记数加一,父进程和子进程对同一内核对象的 Handle 一定是相同的。

复制内核对象的过程是由 CreateProcess 内部完成的,我们可以放心的把对象 Handle (和子进程相同) 通过命令行传递给子进程。或者,用环境变量传递也可以。

值得注意的是,子进程用完了这个 FileMapping 对象后一样需要 CloseHandle 减去引用计数。

备注:
CreateProcess 调用时,pszCommandLine 不能直接填上一个不可修改的字符串。例如:

CreateProcess("test.exe","test argument",...);

这样就是错误的,因为 "test argument" 会被编译器编译放到不可修改的数据段中。正确的方法是:

char cmdline[]="test argument";
CreateProcess("test.exe",cmdline,...);

这样,命令行的字符串就被放在堆栈上,是可以被读写的。

CreateProcess 的倒数第二个参数需要填写一个 STARTUPINFOW 结构,这个结构很复杂,通常填起来很麻烦。我们可以复制一份父进程的结构,再酌情修改。方法是:

STARTUPINFO si={sizeof(si)};
PROCESS_INFORMATION pi;
GetStartupInfo(&si);
CreateProcess(...,&si,& pi);

这里, STARTUPINFO 结构的第一个长度信息通常应该填上,保证 GetStartupInfo(&si); 的正确执行。

2008年7月25日星期五

用Debug函数实现API函数的跟踪 - 一叶障目 不见全貌 - CSDNBlog

用Debug函数实现API函数的跟踪 - 一叶障目 不见全貌 - CSDNBlog
用Debug函数实现API函数的跟踪 (1)
作者:彭春华 来源:赛迪网 发布时间:2003.01.16
【Java专区】 【网络安全】 【网管专区】 【linux专区】 【进入论坛】 【IT博客】 


如果我们能自己编写一个类似调试器的功能,这个调试器需要实现我们对于跟踪监视工具的要求,即自动记录输入输出参数,自动让目标进程继续运行。下面我们就来介绍在不知道函数原型的情况下也可以简单输出监视结果的方案——用Debug函数实现API函数的监视。

用Debug函数实现API函数的监视
大家知道,VC可以用来调试程序,除了调试Debug程序,当然也可以调试 Release程序(调试Release程序时为汇编代码)。如果知道函数的入口地址,只需在函数入口上设置断点,当程序调用了设置断点的函数时,VC就会暂停目标程序的运行,你就可以得到目标程序内存的所有你希望得到的东西了。一般来说,只要你有足够的耐心和毅力,以及一些汇编知识,对于监视API函数的输入输出参数还是可以完成的。
不过,由于VC的调试器会在每次断点时暂停目标程序的运行,对目标程序的过多的暂停对于监视任务而言实在不能忍受。所以,不会有太多的人真的会用VC的调试器作为一个良好的API函数监视器的。
如果VC调试器能够在你设置好断点后,在运行时自动输出断点时的堆栈值(也就是函数的输入参数),在函数运行结束时也自动输出堆栈值(也就是函数的输出参数)和CPU寄存器的值(就是函数返回值),并且不会暂停目标程序。所有一切都是自动的无需我们干预。你会用它来作为监视器吗?我会的。
我不知道如何让VC这样作(或许VC真的可以这样,但我不知道。有人知道的话请通知我一声,谢谢),但我知道显然VC也是通过调用 Windows API函数完成调试器的任务,而且,这些函数显然可以实现我的要求。我需要作的事情就是自己利用这些API函数,写一个简单的调试器,在目标程序断点发生时自动输出监视结果并且自动恢复目标程序的运行。
显然,用VC调试器作为监视器的话无需知道目标函数的原型就可以得到简单的输入输出参数和函数运行结果,而且,由于监视代码没有注入目标程序中,就不会出现监视目标函数和监视代码的冲突。VC调试器显然可以跟踪递归函数,也可以跟踪DLL模块调用DLL本身的函数,以及EXE内部调用自身的函数。只要你知道目标函数的入口地址,就可以跟踪了(监视Exe自身的函数可以通过生成Exe模块时选择输出Map文件,就可以参考Map文件得到Exe内部函数的地址)。没有听说VC不能调试多线程的,最多是说调试多线程比较麻烦----证明多线程是可以调试的。显然,VC也可以调试DllMain中的代码。这些,已经可以证明通过调试函数可以实现我们的目标了。

如何编写实现我们目标的程序?需要哪些调试函数?
首先,让目标程序进入被调试状态:
对于一个已经启动的进程而言,利用DebugActiveProcess函数就可以捕获目标进程,将目标进程进入被调试状态。
BOOL DebugActiveProcess(DWORD dwProcessId);


参数dwProcessId是目标进程的进程ID。如何通过ToolHelp系列函数或Psapi库函数获得一个运行程序的进程ID在很多文章中介绍过,这里就不再重复。对于服务器程序而言,由于没有权限无法捕获目标进程,可以通过提升监视程序的权限得到调试权限进行捕获目标进程(用户必须拥有调试权限)。
对于启动一个新的程序而言,通过CreateProcess函数,设置必要的参数就可以将目标程序进入被调试状态。
BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES
lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID
lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation );


该函数的具体说明请参考MSDN,在这里我仅介绍我们感兴趣的参数。这里和一般的用法不同,作为被调试程序 dwCreationFlags必须设置为DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。这样启动的目标程序就会进入被调试状态。这里说明一下DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS。DEBUG_ONLY_THIS_PROCESS 就是只调试目标进程,而DEBUG_PROCESS参数则不仅调试目标进程,而且调试由目标进程启动的所有子进程。比如:在A.exe中启动B.exe,如果用DEBUG_ONLY_THIS_PROCESS启动,监视进程只调试A.exe不会调试B.exe,如果是DEBUG_PROCESS就会调试 A.exe和B.exe。为简单起见,本文只讨论启动参数为DEBUG_ONLY_THIS_PROCESS的情况。
使用方法:
STARTUPINFO st = {0};
PROCESS_INFORMATION pro = {0};
st.cb = sizeof(st);
CreateProcess(NULL, pszCmd, NULL, NULL, FALSE,
DEBUG_ONLY_THIS_PROCESS,
NULL, szPath, &st, &pro));
// 关闭句柄---这些句柄在调试程序中不再使用,所以可以关闭
CloseHandle(pro.hThread);
CloseHandle(pro.hProcess);


其次,对进入被调试状态的程序进行监视:
目标进程进入了被调试状态,调试程序(这里调试程序就是我们的监视程序,以后不再说明)就负责对被调试的程序进行调试操作的调度。调试程序通过 WaitForDebugEvent函数获得来自被调试程序的调试消息,调试程序根据得到的调试消息进行处理,被调试进程将暂停操作,直到调试程序通过 ContinueDebugEvent函数通知被调试程序继续运行。
BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, // debug event information
DWORD dwMilliseconds // time-out value
);


在参数lpDebugEvent中可以获得调试消息,需要注意的是该函数必须和让目标程序进入调试状态的线程是同一线程。也就是说和通过DebugActiveProcess或CreateProcess调用的线程是一个线程。另外,我又喜欢将dwMilliseconds设置为 -1(无限等待)。所以我通常都会将CreateProcess和WaitForDebugEvent函数在一个新的线程中使用。
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;


在这个调试消息结构体中,dwDebugEventCode记录了产生调试中断的消息代码。消息代码的详细说明可以参考MSDN。其中,我们感兴趣的消息代码为:
EXCEPTION_DEBUG_EVENT:产生调试例外
CRATE_THREAD_DEBUG_EVENT:新的线程产生
CREATE_PROCESS_DEBUG_EVENT:新的进程产生。注:在DEBUG_ONLY_THIS_PROCESS时只有一次,
在DEBUG_PROCESS时如果该程序启动了子进程就可能有多次。
EXIT_THREAD_DEBUG_EVENT:一个线程运行中止
EXIT_PROCESS_DEBUG_EVENT:一个进程中止。注:在DEBUG_ONLY_THIS_PROCESS时只有一次,
在DEBUG_PROCESS可能有多次。
LOAD_DLL_DEBUG_EVENT:一个DLL模块被载入。
UNLOAD_DLL_DEBUG_EVENT:一个DLL模块被卸载。


在得到目标程序的调试消息后,调试程序根据这些消息代码进行不同的处理,最后通知被调试程序继续运行。
BOOL ContinueDebugEvent(
DWORD dwProcessId, // process to continue
DWORD dwThreadId, // thread to continue
DWORD dwContinueStatus // continuation status
);


该函数通知被调试程序继续运行。
使用例:
DEBUG_EVENT dbe;
BOOL rc;
CreateProcess(NULL, pszCmd, NULL, NULL, FALSE,
DEBUG_ONLY_THIS_PROCESS,
NULL, szPath, &st, &pro));
while(WaitForDebugEvent(&dbe, INFINITE))
{
// 如果是退出消息,调试监视结束
if(dbe. dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)
break;
// 进入调试监视处理
rc = OnDebugEvent(&dbe);
if(rc)
ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId , DBG_CONTINUE );
else
ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId ,
DBG_ DBG_EXCEPTION_NOT_HANDLED);
}
// 调试消息处理程序
BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent)
{
// 我们还没有对目标进程进行操作,所以,先返回TRUE。
return TRUE;
}


上面这些程序就是一个最简单的调试程序了。不过,它基本上没有什么用途。你还没有在目标进程中设置断点,你就不能完成对API函数监视的任务。

对目标进程设置断点:
我们的目标是监视API函数的输入输出,那么,首先应该知道DLL模块中提供了哪些API函以及这些API的入口地址。在前面将过,广义的 API还包括未导出的内部函数。如果你有DLL模块的调试版本和调试连接文件(pdb文件),也可以根据调试信息得到内部函数的信息。
· 得到函数名及函数入口地址
通过程序得到函数的入口地址有很多种方法。对于用VC编译出来的DLL,如果是Debug版本,可以通过ImageHlp库函数得到调试信息,分析出函数的入口地址。如果没有Debug版本,也可以通过分析导出函数表得到函数的入口地址。
1.用Imagehlp库函数得到Debug版本的函数名和函数入口地址。
可以利用Imagehlp库函数分析Debug信息,关联的函数为SymInitialize、SymEnumerateSymbols和 UnDecorateSymbolName。详细可以参考MSDN中关于这些函数的说明和用法。不过,用Imagehlp只能分析出用VC编译的程序,对 C++Builder编译的程序不能用这种方法分析。
2.DLL的导出表得到函数导出函数名和函数的入口地址。
在大多数情况下,我们还是希望监视的是Release版本的输入输出参数,毕竟Debug版本不是我们最终提供给用户的产品。Debug和 Release的编译条件不同导致产生的结果不同,在很多BBS中都讨论过。所以,我认为跟踪监视Release版本更加有实用价值。
通过分析DLL导出表得到导出函数名在MSDN上就有源代码。关于导出表的说明大家可以参考关于PE结构的文章。
3.通过OLE函数取得COM接口
你也可以通过OLE函数分析DLL提供的接口函数。接口函数不是通过DLL导出表导出的。你可以通过LoadTypeLib函数来分析COM接口,得到COM记录接口的入口地址,这样,你就可以监视COM接口的调用了。这是API HOOK没法实现的。在这里我不打算分析分析COM接口的方式了。在MSDN上通过搜索LoadTypeLib sample关键词你就可以找到相关的源代码进行修改实现你的目标。
这里是通过计算机自动分析目标模块得到DLL导出函数的方案,作为我们监视的目的而言,这些工作只是为了得到一系列的函数名和函数地址而已。函数名只是一个让我们容易识别函数的名称而已,该函数入口地址才是我们真正关心的目标。换句话说,如果你能够确保某一个地址一定是一个函数(包括内部函数)的入口地址,你就完全可以给这个函数定义自己的名称,将它加入你的函数管理表中,同样可以实现监视该函数的输入输出参数的功能。这也是实现Exe内部函数的监视功能的原因。如果你有Exe编译时生成的Map文件(你可以在编译时选择生成Map文件),你就可以通过分析Map文件,得到内部函数的入口地址,将内部函数加入到你的函数管理表中。(一个函数的名称对于监视功能来讲究竟是FunA还是FunB并没有什么意义,但名称是FunA还是FunB的名称对于监视者分析监视结果是有意义的,你完全可以将MessageBox的函数在输出监视结果是以FunA的名称输出,所以在监视一些内部无名称的函数时,你完全可以定义你自己的名字)。
· 在函数入口地址处设置断点
设置断点非常简单,只要将0xCC(int 3)写入指定的地址就可以了。这样程序运行到指定地址时,将产生调试中断信息通知调试程序。修改指定进程的内存数据可以通过 WriteProcessMemory函数来完成。由于一般情况下作为程序代码段都被保护起来了,所以还有一个函数也会用到。 VirtualProtectEx。在实际情况下,当调试断点发生时,调试程序还应该将原来的代码写回被调试程序。
unsigned char SetBreakPoint(DWORD pAdd, unsigned char code)
{
unsigned char b;
BOOL rc;
DWORD dwRead, dwOldFlg;
// 0x80000000以上的地址为系统共有区域,不可以修改
if( pAdd >= 0x80000000 || pAdd == 0)
return code;
// 取得原来的代码
rc = ReadProcessMemory(_ghDebug, pAdd, &b, sizeof(BYTE), &dwRead);
// 原来的代码和准备修改的代码相同,没有必要再修改
if(rc == 0 || b == code)
return code;
// 修改页码保护属性
VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), PAGE_READWRITE,
&dwOldFlg);
// 修改目标代码
WriteProcessMemory(_ghDebug, pAdd, &code, sizeof(unsigned char), &dwRead);
// 恢复页码保护属性
VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), dwOldFlg, &dwOldFlg);
return b;
}


在设置断点时你必须将原来的代码保存起来,这样在恢复断点时就可以将代码还原了。一般用法为:设置断点m_code = SetBreakPoint( pFunAdd, 0xCC); 恢复断点:SetBreakPoint( pFunAdd, m_code); 记住,每个函数入口地址的代码都可能不同,你应该为每个断点地址保存一个原来的代码,在恢复时就不会发生错误了。
好了,现在目标程序中已经设置好了断点,当目标程序调用设置了断点的函数时,将产生一个调试中断信息通知调试程序。我们就要在调试程序中编写我们的调试中断程序了。

编写调试中断处理程序
被调试程序产生中断时,将产生一个EXCEPTION_DEBUG_EVENT信息通知调试程序进行处理。同时将填充EXCEPTION_DEBUG_INFO结构。
typedef struct _EXCEPTION_DEBUG_INFO {
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;


在该结构中,我们比较感兴趣的是产生中断的地址ExceptionAddress和产生中断的信息代码ExceptionCode。在信息代码中与我们任务相关的信息代码为:
EXCEPTION_BREAKPOINT:断点中断信息代码
EXCEPTION_SINGLE_STEP:单步中断信息代码


断点中断是由于我们在前面设置断点0xCC代码运行时产生的。由于产生中断后,我们必须将原来的代码写回被调试程序中继续运行。但是,代码一旦被写回目标程序,这样,当目标程序再次调用该函数时将不会产生中断,我们就只能实现一次监视了。所以,我们必须在将原代码写回被调试程序后,应该让被调试程序已单步的方式运行,再次产生一个单步中断的调试信息。在单步中断处理中,我们再次将0xCC代码写入函数的入口地址,这样就可以保证再次调用时产生中断。
首先,在进行中断处理前我们必须作些准备工作,管理起线程ID和线程句柄。为了管理单步中断处理,我们还必须维护一个基于线程的单步地址的管理,这样就可以允许被调试程序拥有多线程的功能。--我们不能保证单步运行时不被该进程的其他线程所打断。
// 我们利用一个map进行管理线程ID和线程句柄之间的关系
// 同时也用一个map管理函数地址和断点的关系
typedef map > THREAD_MAP;
typedef map > THREAD_SINGLESTEP_MAP;
THREAD_MAP _gthreads;
FUN_BREAK_MAP _gFunBreaks;
// 并且假设设置断点时采用了如下方案进行原来代码的管理
BYTE code = SetBreakPoint(pFunAdd, 0xCC);
if(code != 0xCC)
_gFunBreaks[pFunAdd] = code;

// 调试处理程序
BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent)
{
BOOL rc = TRUE;
switch(pEvent->dwDebugEventCode)
{
case CREATE_PROCESS_DEBUG_EVENT:
// 记录线程ID和线程句柄的关系
_gthreads[pEvent->dwThreadId] = pEvent->u.CreateProcessInfo.hThread;

break;
case CREATE_THREAD_DEBUG_EVENT:
// 记录线程ID和线程句柄的关系
_gthreads [pEvent->dwThreadId] = pEvent->u.CreateThread.hThread;

break;
case EXIT_THREAD_DEBUG_EVENT:
// 线程退出时清除线程ID
_gthreads.erase (pEvent->dwThreadId);

break;
case EXCEPTION_DEBUG_EVENT:
// 中断处理程序
rc = OnDebugException(pEvent);
break;

}
return rc;
}


下面进行中断处理程序。同样,我们只考虑我们关心的中断信息代码。在发生中断时,我们通过 GetThreadContext(&context)得到中断线程的上下文信息。此时,context.esp就是函数的返回地址,context.esp+4位置的值就是函数的第一个参数,context.esp+8就是第二个参数,依次类推可以得到你想要的任何参数。需要注意的是因为参数是在被调试进程中的内容,所以你必须通过ReadProcessMemory函数才能得到:
DWORD buf[4]; // 取4个参数
ReadProcessMemory(_ghDebug, (void*)(context.esp + 4), &buf, sizeof(buf),
&dwRead);


那么buf[0]就是第一个参数,buf[1]就是第二个参数。。。注意,在FunA(int a, char* p, OPENFILENAME* pof)函数调用时,buf[0] = a, buf[1] = p这里buf[1]是p的指针而不是p的内容,如果你希望访问p的内容,必须同样通过ReadProcessMemory函数再次取得p的内容。对于结构体指针也必须如此:
// 取得p的内容:
char pBuf[256];
ReadProcessMemory(_ghDebug, (void*)(buf[1]), &pBuf, sizeof(pBuf), &dwRead);
//取得pof的内容:
OPENFILENAME of
ReadProcessMemory(_ghDebug, (void*)(buf[2]), &of, sizeof(of), &dwRead);


如果结构体中还有指针,要取得该指针的内容,也必须和取得p的内容一样的方式读取被调试程序的内存。总的来说,你必须意识到监视目标程序的所有内容都是对目标进程的内存读取操作,这些指针都是目标进程的内存地址,而不是调试进程的地址。
很明显,当被调试进程在函数入口产生中断调试信息时,调试程序只能得到函数的输入参数,而不能得到我们希望的输出参数及返回值!为了实现我们的目标,我们必须在函数调用结束时,再次产生中断,取得函数的输出参数和返回值。在处理函数入口中断时,就必须设置好函数的返回地址的断点。这样,在函数返回时,就可以得到函数的输出参数和返回值了。关于这里的实现说明请参考附录的源代码。
你完全可以参照附录的源代码写出你自己的简单的调试监视程序。当然,有几个问题因为比较复杂,我没有在这里进行说明。一个就是函数返回断点的处理,比如TRY、CATCH的处理,就必须重新设计好RETURN_FUN_STACK的结构,考虑一些除错处理还是可以解决这个问题的。另外一个问题就是函数的入口断点和返回断点没有任何关系。这个问题更好解决,只需重新设计RETURN_FUN,FUN_BREAK_MAP等结构体就可以将它们关联起来。由于我在这里只要是分析如何实现中断调试处理的过程,这些完善程序的工作就由读者自行跟踪改造了。

关于Win9X系统
细心的读者在上面可以发现一个问题,那就是在SetBreakPoint函数中有一个限制,就是函数的入口地址不能大于0x80000000。确实如此,我们知道0x80000000以上的空间是系统共有的空间,我们一般不能修改这些空间的程序,否则将影响系统的工作。在NT环境下,所有的DLL都被加载在0x80000000下,修改0x80000000以下空间的代码不会对其它进程产生影响。所以在NT下可以用上面的方案监视所有的DLL函数。然而,在Win9X下,kernel32.dll,user32.dll,gdi32.dll等系统DLL都被加载到0x80000000以上的空间,修改这些空间的代码将破坏系统工作。那么,在9X下就不能监视这些DLL模块的函数吗?
的确,在Win9X平台下不能利用在函数入口处设置断点的方法实现监视。我们必须采用另外的方法实现该功能。在前面讨论中知道,通过API HOOK修改模块导入表的方法可以实现将API的入口修改为自己监视程序的入口,也可以实现监视功能。如果采用API HOOK的方法有限制,即必须知道函数原型,对每一个函数都必须编写相应的监视代码,灵活性受到限制。而我们的目标是不管有多少个DLL,不管DLL有多少个导出函数,在不修改我们的程序前提下都可以实现我们的监视功能。所以,API HOOK是不可以完成我们的目标,但我们可以利用修改导入表的方案实现目标。首先,修改导入表,将函数的调用地址指向我们的监视代码,在监视代码中,我们无需对函数编程,只是简单调用jmp XXXX就可以了。然后,设置断点时,不是设置在函数的入口点,而是设置在我们的监视代码上。这样,当我们的模块调用系统API函数时,就可以实现监视功能了。修改原理如图:


如图所示,假设我们的监视代码在目标进程的的0x20000000空间,我们在分析DLL导出表的同时,将导出表函数的地址经过计算,在监视代码中设置为jmp xxxx的代码。这样我们在修改EXE模块的导入表时写入的地址为监视代码的地址。当目标程序调用MessageBox函数是,程序将首先跳转到监视代码中执行jmp指令到user32.dll的MessageBox入口地址中。经过这样处理后,我们希望监视MessageBox函数的调用时,只需在监视代码的0x20000000处设置断点,就达到了监视的目的。限于篇幅原因,这里不再讨论。

扩展应用
你可以很轻松的在此基础上进行扩展你的监视跟踪功能。只需要修改一下记录输入输出函数结果的程序,就得到一个新的功能:
1.在记录输入输出参数的地方加入取得当前时刻的功能,就实现了监视函数调用性能的功能。(相当于Numega的TrueTime功能)由于采用了Debug技术,得到的时间将包括调试函数导致产生进程的切换时间。等到的时间只是一个参考价值,但对分析性能而言一般足够。
2.在记录输入输出参数的地方加入函数调用的计数器,就实现了Numega的TrueCoverage功能。
3.监视malloc, free, realloc函数的输入输出值,并进行统计,就实现了简单的内存泄漏检查功能。关键的是你可以通过Map文件得到Release版本的malloc等函数的地址,实现对Release版的跟踪。
4.在记录输入参数处理中加入StackWalk函数可以实现call stack功能,分析是由哪个函数调用了自己。在jmp方案中也可以实现这个功能,但是你必须确保StackWalk关联的函数没有调用被你监视的函数。在Hook API(IAT)的方案中到是不用保证,但得出的调用列表中有可能包含你的监视代码。
有一点需要注意的是,我们的目标是监视程序的运行路径,并不是改变参数和修改结果,所以,在jmp和Hook Api(IAT)中可以实现的修改参数和运行路径的做法在这里不能实现。
其他:
本文附录的代码TestDebug.zip就是实现了一个简单的调试监视器,自动输出监视函数的4个输入参数的地址内容和函数调用返回值。该代码只是表明通过监视函数可以实现对API的跟踪,所以没有实现9X下对系统DLL的监视。
DebugApi.zip是一个利用这个方案编写的应用程序DebugApiSpy.exe,它实现了这个方案中的最基本的跟踪监视函数的输入输出参数功能,也实现了9X下对系统DLL的监视支持。该程序支持Win9X/NT/W2K/XP上的运用。
源代码下载:TestDebug.zip,DebugApi.zip
参考资料:
1.《Windows核心编程》, Jeffrey Richter,机械工业出版社
2.微软的MSDN
3.detours 可以在http://research.microsoft.com/sn/detours/ 上得到源代码。detours功能在WinNT和W2K下有效,对9X不支持。

2008年7月23日星期三

[PKI]ActiveX控件签名(转)  - 么么茶.NET - 博客园

[PKI]ActiveX控件签名(转)  - 么么茶.NET - 博客园

[PKI]ActiveX控件签名(转) 

一,使用微软的工具不采用私钥文件

1.制作根证书

makecert -sk "myPK" -ss mySSName -n "CN=公司名称" -r myroot.cer

sk-表示主题的密钥容器位置,ss-主题的证书存储名称, n-证书颁发对象,r-证书存储位置;

2.制作子证书

makecert -sk "myPK" -is mySSName -n "CN=公司名称" -$ commercial -ic myroot.cer test.cer
sk-表示主题的密钥容器位置,is-颁发者的证书存储名称, n-证书颁发对象,ic-颁发者的证书存储位置,-$-授权范围(用于代码签名);

3.使用Cert2Spc生成spc发行者证书

cert2spc test.cer test.spc

4.使用signcode为你的程序,库或cab包签名:
双击signcode,或在控制台键入signcode,不带参数会启动签名向导。在第三步选择“自定义选项”,第四步选择“从文件选择”选择test.spc或test.cer,第五步选择“CSP中的私钥”,在密钥容器中选择我们定义的myPK,其他步骤默认即可,如果想添加时间戳,请在时间戳服务器地址上键入:(免费时间戳认证)
http://timestamp.wosign.com/timestamp
完成后,观察你所签名的文件属性,应该已经添加数字签名项。

5.将myroot.cer导入“受信任的根证书颁发机构”,使用chktrust测试刚才的文件是否签名成功

二,使用微软的工具采用私钥文件

1.制作根证书

makecert -sv "myroot.pvk" -ss mySSName -n "CN=公司名称" -r myroot.cer

sv-私钥文件名,ss-主题的证书存储名称, n-证书颁发对象,r-证书存储位置;

2.制作子证书

makecert -sv "test.pvk" -iv myroot.pvk -n "CN=公司名称" -$ commercial -ic myroot.cer test.cer
sv-私钥文件名,iv-根证书的私钥文件, n-证书颁发对象,ic-颁发者的证书存储位置,-$-授权范围(用于代码签名);

3.使用Cert2Spc生成spc发行者证书

cert2spc test.cer test.spc

4.使用signcode为你的程序,库或cab包签名:
双击signcode,或在控制台键入signcode,不带参数会启动签名向导。在第三步选择“自定义选项”,
第四步选择“从文件选择”选择test.spc或test.cer,
第五步选择“文件中的私钥”选择test.pvk,其他步骤默认即可,如果想添加时间戳,请在时间戳服务器地址上键入:(免费时间戳认证)
http://timestamp.wosign.com/timestamp
完成后,观察你所签名的文件属性,应该已经添加数字签名项。

用命令方式:signcode -spc test.spc -v test.pvk -n test的软件 test.cab

注意:用signcode.exe签署自己的软件。假如是.cab文件,需要在用cabarc.exe制作的时候
用-s参数留出签名的空间(一般6144字节即可)。

5.将myroot.cer导入“受信任的根证书颁发机构”,使用chktrust测试刚才的文件是否签名成功

三,使用openssl产生根证书

1.用openssl创建CA证书的RSA密钥(PEM格式):
openssl genrsa -des3 -out ca.key 1024

2.用openssl创建CA证书(PEM格式,假如有效期为一年):
openssl req -new -x509 -days 365 -key ca.key -out ca.crt -config openssl.cnf

openssl是可以生成DER格式的CA证书的,很奇怪Windows却说那证书是“无效的”,
无奈,只好用IE将PEM格式的CA证书转换成DER格式的CA证书。

3.将ca.crt导入至IE中。
导入时注意一定要将证书存储至“本地计算机”。
具体步骤如下:
1)在“我的电脑”或“资源管理器”里双击该文件图标。
2)在“常规”卡片上选择“安装证书”。
3)点“下一步”至“证书导入向导”,选择“将所有的证书放入下列存储区”,
点下面的“浏览”。勾上“显示物理存储区”。选择“受信任的根目录...”下一级的
“本地计算机”。点“确定”,再点“下一步”。
4)点“完成”。

可以检查一下导入是否完全成功:
在IE的Internet选项中的“证书”中“受信任根证书颁发机构”中应该可以
看见上述的根证书。

4.IE的Internet选项中的“证书”中“受信任根证书颁发机构”中将刚才
导入的证书导出。格式为“DER编码的二进制X.509(.CER)”。
假设导出的文件名为ca.cer

5.将PEM格式的ca.key转换为Microsoft可以识别的pvk格式。
pvk -in ca.key -out ca.pvk -nocrypt -topvk

6.步骤接第二种方式的第3步

VC编译调试开关--版权没有,盗版不究. ---☆ヅ吹雪

VC编译调试开关--版权没有,盗版不究. ---☆ヅ吹雪

VC编译调试开关
关键词: VC 编译 调试 开关

一、调试版本与发布版本

  有时程序能在调试版本运行但不能运行于发布版本,反之也有可能。一般说来,一个发布版本意味着某些类型的优化,而一个调试版本则没有优化。下面我们来看看它们的区别:

1、特别针对调试版本的编译选项

(1)/MDd,/MLd或者/MTd

  调试版本的运行时刻库有调试符号,使用了调试堆,调试堆的目的是发现内存破坏和内存泄漏,并且向用户报告源代码的哪个地方出了问题。特性:

.调试版本的运行时刻库对内存的分配作了跟踪,允许用户检查内存泄漏。

.在刚分配的内存里写上0xCD的字节模式,用0xCD来填充刚分配的内存,有助于发现数据未被初始化的错误。

.在被释放的内存写上0xDD的字节模式,有助于发现已被释放的内存。

.在缓冲区的两边分配了四字节的保护数据,并用0xFD的字节模式作初始化,来检查写内存的上溢出和下溢出。

.在每个内存分配的地方对源代码文件名和行号作了记录,有助于用户在源代码中对内存分配进行定位。

(2)/Od

  这个选项用来关闭优化开关。因为未被优化的代码直接对应于源代码,所以比优化后的代码更容易读懂。未被优化的代码编译和链接会更快,会有更短的调试周期。而由于优化,发布版本不见得会比调试版本运行得好,优化代码要求编译器做一些假设,去除冗余,但有时这个假设是错误的,并且去掉的冗余也有可能隐藏错误。如发布版本的帧指针(EBP寄存器)省略(FPO)隐藏了函数原型不匹配的错误;在同步异常模式(只能由throw语句抛出,编译器默认,由/GX编译选项设置)下,异常处理程序可能被优化掉,会阻止程序中的C++异常处理代码安全地捕获结构异常,在这种情况下,你必须使用异步异常模式(采取任何指令都会产生异常的机制,由/Eha编译选项设置)。

(3)/D “_DEBUG”

  打开条件编译调试代码开关。只有这个符号被定义,调试代码才会被编译,MFC使用_DEBUG符号来确定到底链接的是哪个版本的MFC类库。在调试版本中,内联默认情况下是被关闭的。

(4)/ZI

  创建编辑继续(Edit and Continue)的程序数据库。这个选项会打开/GF编译选项,/GF编译选项会消除重复字符串,并将字符串放到只读内存。编辑继续功能需要获取存储在 PDB文件里的特殊信息来使得代码的修改对调试器有效。如果被修改文件对应的信息不在PDB文件里,编辑继续功能就不能进行,而且在调试过程中对代码的任何修改都会出现下面的提示信息“One or more files are out of date or do not exist.”。

(5)/GZ

在调试版本中用来发现那些在发布版本里才发现的错误。其作用如下:

.用0xCC模式初始化自动(本地)变量。

.在通过函数指针调用函数时,检查栈指针,确认是否有调用规则不匹配。

.在函数最后检查栈指针是否被改变。

(6)/Gm

  打开最小化重新链接开关,减少链接时间。

2、特别针对发布版本的编译选项

(1)/MD,/ML或者/MT

  使用发布版本的运行时刻库。

(2)/O1或者/O2

  打开优化开关,使得程序会最小或说速度会最快,优化器还可能发现代码中潜在的错误,而这些错误可能会被调试版本掩盖。

(3)/D “NDEBUG”

  关闭条件编译调试代码开关。

(4)/GF

  消除重复字符串并将它们放到只读内存中以避免被错误地修改。

(5)/Zi

创建包含调试符号的程序数据库。

  如果一个错误只发生在发布版本里,除非你是个汇编高手,否则你需要调试符号来提示你到底程序出现了什么问题,调试符号保存在程序的数据库文件(PDB)中。Visual C++的AppWizard默认情况下没有为发布版本创建调试符号。为创建调试符号,打开工程设置对话框,选择Win32 Release,在C/C++标签里选择Common类,在调试信息里,如果是发布版本选择Program Database,如果是调试版本选择Program Database for Edit and Continue(编辑继续选项与优化链接不相容,不适于发布版本)。在Link标签里选择Debug类,然后选择Debug Info和Microsoft format选项,最好不要选择Separate types选项,这样所有的调试信息才会被合并到单独的一个PDB文件中。对于发布版本,选择Link标签,在Project options对话框的最后加上“/OPT:REF”,这个选项使得不被引用的函数和数据不会出现在可执行文件中,避免了文件的无谓增大。对于调试版本不要使用这个选项,它会关闭增量链接(incremental linking)。

1、使用最高的编译警告级别/W4

  象 if(x=2)这样的语句,默认的警告级别为/W3时不显示任何信息,但改成最高警告级别/W4时则会出现“waning C4706:assignment within conditional expression”的警告。/W4能给出一些/W3所不能给的警告。

2、在调试版本中使用/GZ编译选项

  /GZ选项用来发现那些在发布版本里才发现的错误,包括未被初始化的自动(局部)变量、堆栈错误、不正确的函数原型等。

3、使用#pragma warning编译器指示

  你可以使用#pragma warning编译器指示来禁止整个程序、特定的头文件、特定的代码文件或是特定的某一行代码的特定警告,这看你把#pragma放在哪里。

4、使用没有警告的编译法则/WX

  这个编译选项把所有的警告当成错误来对待,只有在假警告被消除之后才能应用。有时编译警告可能是合理的,处理编译警告的核心是要发现错误,而不是抑制警告本身。这个法则对于大的程序开发小组来说很有帮助。最终目标是消除错误,而不是消除警告。

对_stdcall 的理解 (转)_风君藏文


对_stdcall 的理解 (转)_风君藏文
: "在C语言中,假设我们有这样的一个函数:

int function(int a,int b)

调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级

语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来"

在C语言中,假设我们有这样的一个函数:

int function(int a,int b)

调用时只要用result = function(1,2)这样的方式就可以使用这个函数。但是,当高级

语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。为此,计算机提供了一种被称为栈的数据结构来支持参数传递。

栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。

在参数传递中,有两个很重要的问题必须得到明确说明:

当参数个数多于一个时,按照什么顺序把参数压入堆栈

函数调用后,由谁来把堆栈恢复原装

在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:

stdcall

cdecl

fastcall

thiscall

naked call

stdcall调用约定

stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall。在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK。

stdcall调用约定声明的语法为(以前文的那个函数为例):

int __stdcall function(int a,int b)

stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈 3)函数

名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸

以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处

翻译成汇编语言将变成:

push 2 第二个参数入栈

push 1 第一个参数入栈

call function 调用参数,注意此时自动把cs:eip入栈

而对于函数自身,则可以翻译为:

push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复mov ebp,esp 保存堆栈指针mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a

add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b

mov esp,ebp 恢复esp

pop ebp

ret 8

而在编译时,这个函数的名字被翻译成_function@8

注意不同编译器会插入自己的汇编代码以提供编译的通用性,但是大体代码如此。其中在函数开始处保留esp到ebp中,在函数结束恢复是编译器常用的方法。

从函数调用看,2和1依次被push进堆栈,而在函数中又通过相对于ebp(即刚进函数时的堆栈指针)的偏移量存取参数。函数结束后,ret 8表示清理8个字节的堆栈,函数自己恢复了堆栈。

cdecl调用约定

cdecl调用约定又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:

int function (int a ,int b) //不加修饰就是C调用约定

int __cdecl function(int a,int b)//明确指出C调用约定

在写本文时,出乎我的意料,发现cdecl调用约定的参数压栈顺序是和stdcall是一样的,参数首先由有向左压入堆栈。所不同的是,函数本身不清理堆栈,调用者负责清理堆栈。由于这种变化,C调用约定允许函数的参数的个数是不固定的,这也是 C语言的一大特色。对于前面的function函数,使用cdecl后的汇编码变成:

调用处

push 1

push 2

call function

add esp,8 注意:这里调用者在恢复堆栈

被调用函数_function处push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复

mov ebp,esp 保存堆栈指针

mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a

add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b

mov esp,ebp 恢复esp

pop ebp

ret 注意,这里没有修改堆栈

MSDN中说,该修饰自动在函数名前加前导的下划线,因此函数名在符号表中被记录为 _function,但是我在编译时似乎没有看到这种变化。由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数,例如对于CRT中的 sprintf函数,定义为:

int sprintf(char* buffer,const char* format,...)

由于所有的不定参数都可以通过format确定,因此使用不定个数的参数是没有问题的。

fastcall

fastcall调用约定和stdcall类似,它意味着:

函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过

从右向左的顺序压栈

被调用函数清理堆栈

函数名修改规则同stdcall

其声明语法为:int fastcall function(int a,int b)

thiscall

thiscall是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成

员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,th

iscall意味着:

参数从右向左入栈

如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针

在所有参数压栈后被压入堆栈。

对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈

为了说明这个调用约定,定义如下类和使用代码:

class A

{

public:

int function1(int a,int b);

int function2(int a,...);

};

int A::function1 (int a,int b)

{

return a+b;

}

#i nclude

int A::function2(int a,...)

{

va_list ap;

va_start(ap,a);

int i;

int result = 0;

for(i = 0 i < a i ++)

{

result += va_arg(ap,int);

}

return result;

}

void callee()

{

A a;

a.function1 (1,2);

a.function2(3,1,2,3);

}

callee函数被翻译成汇编后就变成:

//函数function1调用

0401C1D push 2

00401C1F push 1

00401C21 lea ecx,[ebp-8]

00401C24 call function1 注意,这里this没有被入栈

//函数function2调用

00401C29 push 3

00401C2B push 2

00401C2D push 1

00401C2F push 3

00401C31 lea eax,[ebp-8] 这里引入this指针

00401C34 push eax

00401C35 call function2

00401C3A add esp,14h

可见,对于参数个数固定情况下,它类似于stdcall,不定时则类似cdecl

naked call

这是一个很少见的调用约定,一般程序设计者建议不要使用。编译器不会给这种函数增加初始化和清理代码,更特殊的是,你不能用return返回返回值,只能用插入汇编返回结果。这一般用于实模式驱动程序设计,假设定义一个求和的加法程序,可以定义为:

__declspec(naked) int add(int a,int b)

{

__asm mov eax,a

__asm add eax,b

__asm ret

}

注意,这个函数没有显式的return返回值,返回通过修改eax寄存器实现,而且连退出函数的ret指令都必须显式插入。上面代码被翻译成汇编以后变成:

mov eax,[ebp+8]

add eax,[ebp+12]

ret 8

注意这个修饰是和__stdcall及cdecl结合使用的,前面是它和cdecl结合使用的代码,对于和stdcall结合的代码,则变成:

__declspec(naked) int __stdcall function(int a,int b)

{

__asm mov eax,a

__asm add eax,b

__asm ret 8 //注意后面的8

}

至于这种函数被调用,则和普通的cdecl及stdcall调用函数一致。函数调用约定导致的常见问题如果定义的约定和使用的约定不一致,则将导致堆栈被破坏,导致严重问题,下面是两种常见的问题:

函数原型声明和函数体定义不一致

DLL导入函数时声明了不同的函数约定

以后者为例,假设我们在dll种声明了一种函数为:

__declspec(dllexport) int func(int a,int b);//注意,这里没有stdcall,使用的是

cdecl

使用时代码为:

typedef int (*WINAPI DLLFUNC)func(int a,int b);

hLib = LoadLibrary(...);

DLLFUNC func = (DLLFUNC)GetProcAddress(...)//这里修改了调用约定

result = func(1,2);//导致错误

由于调用者没有理解WINAPI的含义错误的增加了这个修饰,上述代码必然导致堆栈被破坏,MFC在编译时插入的checkesp函数将告诉你,堆栈被破坏了。