首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
81 阅读
2
nvim番外之将配置的插件管理器更新为lazy
59 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
Java
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
309
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
309
篇与
的结果
2016-07-05
C++多态
面向对象的程序设计的三大要素之一就是多态,多态是指基类的指针指向不同的派生类,其行为不同。多态的实现主要是通过虚函数和虚表来完成,虚表保存在对象的头四个字节,要调用虚函数必须存在对象,也就是说虚函数必须作为类的成员函数来使用。编译器为每个拥有虚函数的对象准备了一个虚函数表,表中存储了虚函数的地址,类对象在头四个字节中存储了虚函数表的指针。下面是一个具体的例子class CVirtual { public: virtual void showNumber(){ printf("%d\n", nNum); } virtual void setNumber(int n){ nNum = n; } private: int nNum; }; int main() { CVirtual cv; cv.setNumber(2); cv.showNumber(); return 0; }上述这段代码定义了两个虚函数setNumber和showNumber,并在主函数中调用了他们,下面通过反汇编的方式来展示编译器是如何调用虚函数的26: CVirtual cv; 00401288 lea ecx,[ebp-8];对象中有一个整形数字占4个字节,同时又有虚函数,头四个字节用来存储虚函数表指针,总共占8个字节 0040128B call @ILT+25(CVirtual::CVirtual) (0040101e);调用构造函数 27: cv.setNumber(2); 00401290 push 2 00401292 lea ecx,[ebp-8] 00401295 call @ILT+0(CVirtual::setNumber) (00401005);调用虚函数 28: cv.showNumber(); 0040129A lea ecx,[ebp-8] 0040129D call @ILT+5(CVirtual::showNumber) (0040100a);调用虚函数 29: return 0; 004012A2 xor eax,eax ;构造函数 0401389 pop ecx;还原ecx使得ecx保存对象的首地址 0040138A mov dword ptr [ebp-4],ecx 0040138D mov eax,dword ptr [ebp-4] 00401390 mov dword ptr [eax],offset CVirtual::`vftable' (0042f020);将虚函数表的首地址赋值到对象的头4个字节 00401396 mov eax,dword ptr [ebp-4] 00401399 pop edi 0040139A pop esi 0040139B pop ebx 0040139C mov esp,ebp 0040139E pop ebp 0040139F ret ;setNumber(int n) 0040134A mov dword ptr [ebp-4],ecx 18: nNum = n; 0040134D mov eax,dword ptr [ebp-4];eax = ecx 00401350 mov ecx,dword ptr [ebp+8] 00401353 mov dword ptr [eax+4],ecx 从上面的汇编代码可以看到,当类中有虚函数的时候,编译器会提供一个默认的构造函数,用于初始化对象的头4个字节,这四个字节存储的是一个指针值,我们定位到这个指针所在的内存如下图:这段内存中存储了两个值,分别为0x0040100AH和0x00401005H,我们执行后面的代码发现,这两个地址给定的是虚函数所在的地址。在调用时编译器直接调用对应的虚函数,并没有通过虚表来寻址到对应的函数地址。下面看另外一个例子:class CParent { public: virtual void showClass() { printf("CParent\n"); } }; class CChild:public CParent { public: virtual void showClass() { printf("CChild\n"); } }; int main() { CParent *pClass = NULL; CParent cp; CChild cc; pClass = &cp; pClass->showClass(); pClass = &cc; pClass->showClass(); pClass = NULL; return 0; }上述代码定义了一个基类和派生类,并且在派生类中重写了函数showClass,在调用时用分别利用基类的指针指向基类和派生类的对象来调用这个虚函数,得到的结果自然是不同的,这样构成了多态。下面是它的反汇编代码,这段代码基本展示了多态的实现原理29: CParent *pClass = NULL; 00401288 mov dword ptr [ebp-4],0 30: CParent cp; 0040128F lea ecx,[ebp-8] 00401292 call @ILT+30(CParent::CParent) (00401023);调用构造函数 31: CChild cc; 00401297 lea ecx,[ebp-0Ch] 0040129A call @ILT+0(CChild::CChild) (00401005) 32: pClass = &cp; 0040129F lea eax,[ebp-8];取对象的首地址 004012A2 mov dword ptr [ebp-4],eax; 33: pClass->showClass(); 004012A5 mov ecx,dword ptr [ebp-4] 004012A8 mov edx,dword ptr [ecx];将指针值放入到edx中 004012AA mov esi,esp 004012AC mov ecx,dword ptr [ebp-4];获取虚函数表指针 004012AF call dword ptr [edx];调转到虚函数指针所对应的位置执行代码 004012B1 cmp esi,esp 004012B3 call __chkesp (00401560) 34: pClass = &cc; 004012B8 lea eax,[ebp-0Ch] 004012BB mov dword ptr [ebp-4],eax 35: pClass->showClass(); 004012BE mov ecx,dword ptr [ebp-4] 004012C1 mov edx,dword ptr [ecx] 004012C3 mov esi,esp 004012C5 mov ecx,dword ptr [ebp-4] 004012C8 call dword ptr [edx] 004012CA cmp esi,esp 004012CC call __chkesp (00401560) 36: pClass = NULL; 004012D1 mov dword ptr [ebp-4],0 37: return 0; 004012D8 xor eax,eax从上述代码来看,在调用虚函数时首先根据头四个字节的值找到对应的虚函数表,然后根据虚函数表中存储的内容来找到对应函数的地址,最后根据函数地址跳转到对应的位置,执行函数代码。由于虚函数表中的虚函数是在编译时就根据对象的不同将对应的函数装入到各自对象的虚函数表中,因此,不同的对象所拥有的虚函数表不同,最终根据虚函数表寻址到的虚函数也就不同,这样就构成了多态。对于虚函数的调用,先后经历了几次间接寻址,比直接调用函数效率低了一些,通过虚函数间接寻址访问的情况只有利用类对象的指针或者引用来访问虚函数时才会出现,利用对象本身调用虚函数时,没有必要进行查表,因为已经明确调用的是自身的成员函数,没有构成多态,查表只会降低程序的运行效率。
2016年07月05日
6 阅读
0 评论
0 点赞
2016-06-29
C++类的构造函数与析构函数
C++中每个类都有其构造与析构函数,它们负责对象的创建和对象的清理和回收,即使我们不写这两个,编译器也会默认为我们提供这些构造函数。下面仍然是通过反汇编的方式来说明C++中构造和析构函数是如何工作的。编译器是否真的会默认提供构造与析构函数在一般讲解C++的书籍中都会提及到当我们不为类提供任何构造与析构函数时编译器会默认提供这样六种成员函数:不带参构造,拷贝构造,“=”的重载函数,析构函数,以及带const和不带const的取地址符重载。但是编译器具体是怎么做的,下面来对其中的部分进行说明不带参构造下面是一个例子:class test { private: char szBuf[255]; public: static void print() { cout<<"hello world"; } }; void printhello(test t) { t.print(); } int main(int argc, char* argv[]) { test t; printhello(t); return 0; }下面是对应的汇编源码:00401400 push ebp 00401401 mov ebp,esp 00401403 sub esp,140h;栈顶向上抬了140h的空间用于存储类对象的数据 00401409 push ebx 0040140A push esi 0040140B push edi 0040140C lea edi,[ebp-140h] 00401412 mov ecx,50h 00401417 mov eax,0CCCCCCCCh 0040141C rep stos dword ptr [edi] 26: test t; 27: printhello(t); 0040141E sub esp,100h从上面可以看到,在定义类的对象时并没有进行任何的函数调用,在进行对象的内存空间分配时仅仅是将栈容量扩大,就好像定义一个普通变量一样,也就是说在默认情况下编译器并不会提供不带参的构造函数,在初始化对象时仅仅将其作为一个普通变量,在编译之前计算出它所占内存的大小,然后分配,并不调用函数。再看下面一个例子:class test { private: char szBuf[255]; public: virtual void sayhello() { cout<<"hello world"; } static void print() { cout<<"hello world"; } }; void printhello(test t) { t.print(); } int main(int argc, char* argv[]) { test t; printhello(t); return 0; }30: test t; 0040143E lea ecx,[ebp-104h] 00401444 call @ILT+140(test::test) (00401091) ;构造函数 004014A0 push ebp 004014A1 mov ebp,esp 004014A3 sub esp,44h 004014A6 push ebx 004014A7 push esi 004014A8 push edi 004014A9 push ecx 004014AA lea edi,[ebp-44h] 004014AD mov ecx,11h 004014B2 mov eax,0CCCCCCCCh 004014B7 rep stos dword ptr [edi] 004014B9 pop ecx 004014BA mov dword ptr [ebp-4],ecx 004014BD mov eax,dword ptr [ebp-4] 004014C0 mov dword ptr [eax],offset test::`vftable' (0042f02c) 004014C6 mov eax,dword ptr [ebp-4] 004014C9 pop edi 004014CA pop esi 004014CB pop ebx 004014CC mov esp,ebp 004014CE pop ebp这段C++代码与之前的仅仅是多了一个虚函数,这个时候编译器为这个类定义了一个默认的构造函数,从汇编代码中可以看到,这个构造函数主要初始化了类对象的头4个字节,将虚函数表的地址放入到这个4个字节中,因此我们得出结论,一般编译器不会提供不带参的构造函数,除非类中有虚函数。下面请看这样一个例子:class Parent { public: Parent() { cout<<"parent!"<<endl; } }; class child: public Parent { private: char szBuf[10]; }; int main() { child c; return 0; } 下面是它的汇编代码:27: child c; 004013A8 lea ecx,[ebp-0Ch] 004013AB call @ILT+100(child::child) (00401069) ;构造函数 ;函数初始化代码略 004013E9 pop ecx 004013EA mov dword ptr [ebp-4],ecx 004013ED mov ecx,dword ptr [ebp-4] 004013F0 call @ILT+80(Parent::Parent) (00401055) ;最后函数的收尾工作,代码略从上面的代码看,当父类存在构造函数时,编译器会默认为子类添加构造函数,子类的构造函数主要是调用父类的构造函数。class Parent { public: virtual sayhello() { cout<<"hello"<<endl; } }; class child: public Parent { private: char szBuf[10]; }; int main() { child c; return 0; }27: child c; 004013B8 lea ecx,[ebp-10h] 004013BB call @ILT+100(child::child) (00401069) ;构造函数 004013FA mov dword ptr [ebp-4],ecx 004013FD mov ecx,dword ptr [ebp-4] 00401400 call @ILT+80(Parent::Parent) (00401055);调用父类的构造函数 00401405 mov eax,dword ptr [ebp-4] 00401408 mov dword ptr [eax],offset child::`vftable' (0042f01c);初始化虚函数表 0040140E mov eax,dword ptr [ebp-4]从上面的代码中可以看到,当父类有虚函数时,编译器也会提供构造函数,主要用于初始化头四个字节的虚函数表的指针。拷贝构造当我们不写拷贝构造的时候,仍然能用一个对象初始化另一个对象,下面是这样的一段代码int main(int argc, char* argv[]) { test t1; test t(t1); printhello(t); return 0; }我们还是用之前定义的那个test类,将类中的虚函数去掉,下面是对应的反汇编代码30: test t1; 31: test t(t1); 0040141E mov ecx,3Fh 00401423 lea esi,[ebp-100h] 00401429 lea edi,[ebp-200h] 0040142F rep movs dword ptr [edi],dword ptr [esi] 00401431 movs word ptr [edi],word ptr [esi] 00401433 movs byte ptr [edi],byte ptr [esi]从这段代码中可以看到,利用一个已有的类对象来初始化一个新的对象时,编译器仍然没有为其提供所谓的默认拷贝构造函数,在初始化时利用串操作,将一个对象的内容拷贝到另一个对象。当类中有虚函数时,会提供一个拷贝构造,主要用于初始化头四个字节的虚函数表,在进行对象初始化时仍然采用的是直接内存拷贝的方式。由于默认的拷贝构造是进行简单的内存拷贝,所以当类中的成员中有指针变量时尽量自己定义拷贝构造,进行深拷贝,否则在以后进行析构时会崩溃。另外几种就不再一一进行说明,它们的情况与上面的相似,有兴趣的可以自己编写代码验证。另外需要注意的是,只要定义了任何一个类型的构造函数,那么编译器就不会提供默认的构造函数。最后总结一下默认情况下编译器不提供这些函数,只有父类自身有构造函数,或者自身或父类有虚函数时,编译器才会提供默认的构造函数。何时会调用构造函数当对一个类进行实例化,也就是创建一个类的对象时,会调用其构造函数。对于栈中的局部对象,当定义一个对象时会调用构造函数对于堆对象,当用户调用new新建对象时调用构造函数对于全局对象和静态对象,当程序运行之处会调用构造函数下面重点说明当对象作为函数参数和返回值时的情况作为函数参数当对象作为函数参数时调用的是拷贝构造,而不是普通的构造函数下面是一个例子代码:class CA { public: CA() { cout<<"构造函数"<<endl; } CA(CA &ca) { cout<<"拷贝构造"<<endl; } private: char szBuf[255]; }; void Test(CA a) { return; } int main() { CA a; Test(a); return 0; }对应的汇编代码如下:33: Test(a); 004013F9 sub esp,100h 004013FF mov ecx,esp 00401401 lea eax,[ebp-100h];eax保存对象的首地址 00401407 push eax 00401408 call @ILT+15(CA::CA) (00401014) 0040140D call @ILT+35(Test) (00401028) ;拷贝构造代码 cout<<"拷贝构造"<<endl; 0040152D push offset @ILT+50(std::endl) (00401037) 00401532 push offset string "\xbf\xbd\xb1\xb4\xb9\xb9\xd4\xec" (0042f028) 00401537 push offset std::cout (00434088) 0040153C call @ILT+170(std::operator<<) (004010af) 00401541 add esp,8 00401544 mov ecx,eax 00401546 call @ILT+125(std::basic_ostream<char,std::char_traits<char> >::operator<<) (00401082) 21: } 0040154B mov eax,dword ptr [ebp-4] 0040154E pop edi 0040154F pop esi 00401550 pop ebx 00401551 add esp,44h 00401554 cmp ebp,esp 00401556 call __chkesp (004025d0) 0040155B mov esp,ebp 0040155D pop ebp 0040155E ret 4从上面的代码来看,当对象作为函数参数时,首先调用构造函数,将参数进行拷贝。作为函数的返回值class CA { public: CA() { cout<<"构造函数"<<endl; } CA(CA &ca) { cout<<"拷贝构造"<<endl; } private: char szBuf[255]; }; CA Test() { CA a; return a; } int main() { CA a = Test(); return 0; }34: CA a = Test(); 0040155E lea eax,[ebp-200h];eax保存的是对象a 的首地址 00401564 push eax 00401565 call @ILT+145(Test) (00401096);调用test函数 0040156A add esp,4 0040156D push eax;函数返回的临时存储区的地址 0040156E lea ecx,[ebp-100h] 00401574 call @ILT+15(CA::CA) (00401014);调用拷贝构造 ;test函数 28: CA a; 004013BE lea ecx,[ebp-100h] 004013C4 call @ILT+10(CA::CA) (0040100f) 29: return a; 004013C9 lea eax,[ebp-100h] 004013CF push eax 004013D0 mov ecx,dword ptr [ebp+8] 004013D3 call @ILT+15(CA::CA) (00401014);调用拷贝构造 004013D8 mov eax,dword ptr [ebp+8];ebp + 8是用来存储对象的临时存储区 通过上面的反汇编代码可以看到,在函数返回时会首先调用拷贝构造,将对象的内容拷贝到一个临时存储区中,然后通过eax寄存器返回,在需要利用函数返回值时再次调用拷贝构造,将eax中的内容拷贝到对象中。另外从这些反汇编代码中可以看到,拷贝构造以对象的首地址为参数,返回新建立的对象的地址。当需要对对象的内存进行拷贝时调用拷贝构造,拷贝构造只能传递对象的地址或者引用,不能传递对象本身,我们知道对象作为函数参数时会调用拷贝构造,如果以对象作为拷贝构造的参数,那么回造成拷贝构造的无限递归。何时调用析构函数对于析构函数的调用我们仍然分为以下几个部分:局部类对象:当对象所在的生命周期结束后,即一般语句块结束或者函数结束时会调用全局对象和静态类对象:当程序结束时会调用构造函数堆对象:当程序员显式调用delete释放空间时调用参数对象下面是一个例子代码:class CA { public: ~CA() { printf("~CA()\n"); } private: char szBuf[255]; }; void Test(CA a) { printf("test()\n"); } int main() { CA a; Test(a); return 0; }下面是它的反汇编代码;Test(a) 0040133A sub esp,100h;在main函数栈外开辟一段内存空间用于保存函数参数 00401340 mov ecx,3Fh;类大小为255个字节,为了复制这块内存,每次复制4字节,共需要63次 00401345 lea esi,[ebp-10Ch];esi保存的是对象的首地址 0040134B mov edi,esp;参数首地址 0040134D rep movs dword ptr [edi],dword ptr [esi];执行复制操作 0040134F movs word ptr [edi],word ptr [esi] 00401351 movs byte ptr [edi],byte ptr [esi];将剩余几个字节也复制 00401352 call @ILT+5(Test) (0040100a);调用test函数 ;调用Test函数 23: printf("test()\n"); 00401278 push offset string "test()\n" (0042f01c) 0040127D call printf (00401640) 00401282 add esp,4 24: } 00401285 lea ecx,[ebp+8];参数首地址 00401288 call @ILT+0(CA::~CA) (00401005)从上面的代码看,当类对象作为函数参数时,首先会调用拷贝构造(当程序不提供拷贝构造时,系统默认在对象之间进行简单的内存复制,这个就是提供的默认拷贝构造函数)然后当函数结束,程序执行到函数大括号初时,首先调用析构完成对象内存的释放,然后执行函数返回和做最后的清理工作函数返回对象下面是函数返回对象的代码:class CA { public: ~CA() { printf("~CA()\n"); } private: char szBuf[255]; }; CA Test() { printf("test()\n"); CA a; return a; } int main() { CA a = Test(); return 0; }30: CA a = Test(); 0040138E lea eax,[ebp-100h];eax保存了对象的地址 00401394 push eax 00401395 call @ILT+20(Test) (00401019) 0040139A add esp,4 31: return 0; 0040139D mov dword ptr [ebp-104h],0 004013A7 lea ecx,[ebp-100h] 004013AD call @ILT+0(CA::~CA) (00401005);调用类的析构函数 004013B2 mov eax,dword ptr [ebp-104h] 32: } ;test函数 24: CA a; 25: return a; 004012AA mov ecx,3Fh 004012AF lea esi,[ebp-10Ch];esi保存的是类对象的首地址 004012B5 mov edi,dword ptr [ebp+8];ebp+8是当初调用这个函数时传进来的类的首地址 004012B8 rep movs dword ptr [edi],dword ptr [esi] 004012BA movs word ptr [edi],word ptr [esi] 004012BC movs byte ptr [edi],byte ptr [esi] 004012BD mov eax,dword ptr [ebp-110h] 004012C3 or al,1 004012C5 mov dword ptr [ebp-110h],eax 004012CB lea ecx,[ebp-10Ch] 004012D1 call @ILT+0(CA::~CA) (00401005);调用析构函数 004012D6 mov eax,dword ptr [ebp+8]当类作为返回值返回时,如果定义了一个变量来接收这个返回值,那么在调用函数时会首先保存这个值,然后直接复制到这个内存中,但是接着执行类的析构函数析构在函数中定义的类对象,接受返回值得这块内存一直等到它所在的语句块结束才调用析构如果不要这个返回值时又如何呢,下面的代码说明了这个问题int main() { Test(); printf("main()\n"); return 0; } 30: Test(); 0040138E lea eax,[ebp-100h] 00401394 push eax 00401395 call @ILT+20(Test) (00401019) 0040139A add esp,4 0040139D lea ecx,[ebp-100h] 004013A3 call @ILT+0(CA::~CA) (00401005) 31: printf("main()\n"); 004013A8 push offset string "main()\n" (0042f030) 004013AD call printf (00401660) 004013B2 add esp,4同样可以看到当我们不需要这个返回值时,函数仍然会将对象拷贝到这块临时存储区中,但是会立即进行析构对这块内存进行回收。
2016年06月29日
6 阅读
0 评论
0 点赞
2016-06-09
结构体和类
在C++中类与结构体并没有太大的区别,只是默认的成员访问权限不同,类默认权限为私有,而结构体为公有,所以在这将它们统一处理,在例子中采用类的方式。类对象在内存中的分布在类中只有数据成员占内存空间,而类的函数成员主要分布在代码段中,不占内存空间,一般对象所占的内存空间大小为sizeof(成员1) + sizeof(成员2) + ... + sizeof(成员n)但是有几种情况不符合这个公式,比如虚函数和继承,空类,内存对齐,静态数据成员。只要出现虚函数就会多出4个字节的空间,作为虚函数表,继承时需要考虑基类的大小,另外出现静态成员时静态成员由于存在于数据段中,并不在类对象的空间中,所以静态成员不计算在类对象的大小中这些不在此处讨论,主要说明其余的三种情况:空类按照上述公式,空类应该不占内存,但是实际情况却不是这样,下面来看一个具体的例子:class Test { public: int Print(){printf("Hello world!\n");} }; int main() { Test test; printf("%d\n", sizeof(test)); return 0; }运行程序发现,输出结果为1,这个结果与我们预想的可能有点不一样,按理来说,空类中没有数据成员,应该不占内存空间才对,但是我们知道每个类都有一个this指针指向具体的内存,以便成员函数的调用,即使定义一个类什么都不写,编译器也会提供默认的构造函数用来初始化类,但是如果类的实例不占内存空间,那么该如何初始化?所以编译器为它分配一个1字节的空间以便初始化this指针。所以空类占一个字节。内存对齐下面看这样一个类class Test { public: short s; int n; };当在程序中定义这样一个类,通过sizeof来输出大小得到的是8,上面的公式又不满足了,我们知道为了程序的运行效率,编译器并不会依次申请内存用于存储变量,而会采用内存对齐的方式,以牺牲一定内存空间的代价来换取程序的效率,这个类的大小为8,也是内存对齐的结果,查看类工各个成员的地址我们发现 n的地址为0x0012ff44,而s的地址为0x0012ff40,s本来是占2个字节,但是n并没有出现在其后的42的位置,我所用的VC++6.0默认采用的是8个字节的对齐方式,假设编译器采用的是n个字节的对齐方式,而类中某成员实际所占内存空间的大小为m,那么该成员所在的内存地址必须为p的整数倍,而p = min(m, n),所以对于s来说,采用的是2个字节的对齐方式,分配到的首地址为40是2的倍数,而其后的整型成员n占4个字节,采用上述公式,得到它的内存地址应该是4的倍数,所以取其后的44作为它的地址,中间有两个字节没有使用,所以这个类占8个字节。下面再来看一个例子:class Test { public: short s; //8 double d; //8 char c; };通过程序得出当前结果体的大小为24,根据上面的分析,首先在为s分配空间的时候采用的是2个字节的对齐方式,假设分配到的地址为0x0012ff40,那么d采用的是8个字节的对齐方式,它的地址应该为0x0012ff48,最后为c分配内存的时候,应该是用1个字节的对齐方式,总共应该占的空间为8 + 8 + 1 = 17但是结果却并不是这样。在内存对齐时编译器实际采用对齐方式是:假设结构体成员的最大成员占n个字节,编译器默认采用m个字节的对齐方式,那么实际对齐大小应该为min(m, n)的整数倍,所以实际采用的是8个字节的对齐方式,而结构体的大小应该是实际对齐方式的整数倍,所以占24个字节。在编写程序时可以使用#pragma pack(n)的方式来改变编译器的默认对齐方式。另外对于嵌套定义的结构体,对齐情况也有少许不同。class One { public: short s; double d; char c; }; class Two { One one; int n; };输出class two的大小为32个字节,嵌套定义的结构体仍然能够满足上述两个法则,首先其中的成员结构体one大小为24,然后另外一个成员n占4个字节,得到总共占28个字节,然后根据第二个对齐的规则在24和8之间取最小值8,可以得到结构体的大小应该为8的整数倍32个字节。类的成员函数类的成员函数在调用时直接利用对象打点调用,在函数中直接使用类中的成员,函数操作的是不同对象的数据成员,能够达到这个目的实际上类的对象在调用类的成员函数时默认传入的第一个参数是一个指向这个对象地址的指针叫做this指针,具体this指针的原理看下面一段代码:class test { private: int i; public: test(){i = 0;} int GetNum() { i = 10; return i; }; }; int main(int argc, char* argv[]) { test t; t.GetNum(); return 0; } 下面对应的反汇编代码:;主函数 24: test t; 00401278 lea ecx,[ebp-4] 0040127B call @ILT+20(test::test) (00401019) 25: t.GetNum(); 00401280 lea ecx,[ebp-4] 00401283 call @ILT+0(test::GetNum) (00401005) 26: return 0; 00401288 xor eax,eax ;GetNum()函数 18: i = 10; 0040130D mov eax,dword ptr [ebp-4] 00401310 mov dword ptr [eax],0Ah 19: return i; 00401316 mov ecx,dword ptr [ebp-4] 00401319 mov eax,dword ptr [ecx]在主函数中定义类的对象时首先会调用其构造函数,在调用函数之前首先通过lea指令获取到对象的首地址并将它保存到了ecx寄存器中,在函数GetNum中,首先是在函数栈中定义了一个局部变量,将这个局部变量的值赋值为10,然后将这个局部变量的值赋值到ecx所在地址的内存中,最后再将这块内存中的值放到eax中作为参数返回。通过这部分代码可以看到,this指针并不是通过参数栈的方式传递给成员函数的,而是通过一个寄存器来传递,但是成员函数中若有参数,则仍然通过参数栈的方式传递参数。通过寄存器传递给成员方法作为this指针,然后根据数据成员定义的顺序和类型进行指针偏移找到对应的内存地址,对其进行操作。类的静态成员静态数据成员类的静态成员与之前所说的函数中的局部静态变量相似,它们都存储在数据段中,它们的生命周期与它们所在的位置无关,都是全局的生命周期,它们的可见性被封装到了它们所在的位置,对于函数中的局部静态变量来说,只在函数中可见,对于在文件中的全局静态变量来说,它们只在当前文件中可见,类中的局部静态变量可见性只在类中可见。类的静态数据成员的生命周期与类对象的无关,这样我们可以通过类名::变量名的方式来直接访问这块内存,而不需要通过对象访问,由于静态数据成员所在的内存不在具体的类对象中,所以在C++中所有类的对象中的局部静态变量都是使用同一块内存区域,随便一个修改了静态变量的值,其他的对象中,这个静态变量的值都会发生变化。静态函数成员类中的函数成员也可以是静态的,下面看一个静态函数成员的例子。class test { public: static void print() { cout<<"hello world"; } }; int main(int argc, char* argv[]) { test t; t.print(); return 0; }下面是对应的汇编代码:21: test t; 22: t.print(); 00401388 call @ILT+80(test::print) (00401055)我们可以看到,在调用类的静态函数时并没有取对象的地址到ecx的操作,也就说,静态成员函数并不会传递this指针,由于静态成员的生命周期与对象无关,可以通过类名直接访问,那么如果静态成员函数也需要传递this指针的话,那么对于这种通过类名访问的时候,它要怎么传递this指针呢。另外由于静态成员函数不传递this指针,这样会造成另外一个问题,如果需要在这个静态函数中操作类的数据成员,那么通过对象调用时,它怎么能找到这个数据成员所在的地址,另外在还没有对象,通过类直接调用时,这个数据成员还没有分配内存地址,所以说在C++中为了避免这些问题直接规定静态函数不能调用类的非静态成员,但是静态数据成员虽然说由所有类共享,但是能够找到对应的内存地址,所以非静态成员函数是可以访问静态数据成员的。类作为函数参数前面在写函数原理的那篇博文时说过结构体是如何参数传递的,其实类也是一样的,当类作为参数时,会调用拷贝构造,拷贝到函数的参数栈中,下面通过一个简单的例子来说明class test { private: char szBuf[255]; public: static void print() { cout<<"hello world"; } }; void printhello(test t) { t.print(); } int main(int argc, char* argv[]) { test t; printhello(t); return 0; }26: test t; 27: printhello(t); 0040141E sub esp,100h 00401424 mov ecx,3Fh 00401429 lea esi,[ebp-100h] 0040142F mov edi,esp 00401431 rep movs dword ptr [edi],dword ptr [esi] 00401433 movs word ptr [edi],word ptr [esi] 00401435 movs byte ptr [edi],byte ptr [esi] 00401436 call @ILT+130(printhello) (00401087) 0040143B add esp,100h从上面的汇编代码上可以看出,在进行参数传递时通过rep mov这个指令来将对象所在内存中的内容拷贝到函数栈中。在函数参数需要对象时,直接传递对象会进行一次拷贝,这样不仅浪费内存空间,而且在效率上不高,可以通过传递指针或者引用的方式来实现,这样只消耗4个字节的空间,而且不用拷贝,如果希望函数中不修改对象的内容,可以加上const限定。类作为函数返回值类作为函数的返回值时也与之前所说的结构体作为函数的返回值类似,都是需要先将类拷贝到对应函数栈外部的内存中,然后在随着函数栈由系统统一回收,在这就不做特别的说明了。但是与作为参数不同,为了安全起见一般不要返回局部变量的指针或者引用,在某些需要返回类对象的场合一般只能返回类对象。
2016年06月09日
4 阅读
0 评论
0 点赞
2016-04-26
数组的剖析
C语言中数组是十分重要的一种结构,数组采用的是连续存储的方式,下面通过反汇编的方式来解析编译器对数组的操作。数组作为局部变量在任意一个函数当中定义的变量都会被当做局部变量,它们的生命周期与函数的调用有关,下面是一个例子:int main() { int nArray[5] = {1, 2, 3, 4, 5}; int num1 = 1; int num2 = 2; int num3 = 3; int num4 = 4; int num5 = 5; printf("%d\n", num1); printf("%d\n", nArray[0]); printf("%d\n", nArray[1]); return 0; }下面是它对应的反汇编代码00401268 mov dword ptr [ebp-14h],1 0040126F mov dword ptr [ebp-10h],2 00401276 mov dword ptr [ebp-0Ch],3 ... 10: int num1 = 1; 0040128B mov dword ptr [ebp-18h],1 11: int num2 = 2; 00401292 mov dword ptr [ebp-1Ch],2 12: int num3 = 3; ... 16: printf("%d\n", num1); 004012AE mov eax,dword ptr [ebp-18h] 004012B1 push eax ... 17: printf("%d\n", nArray[0]); 004012BF mov ecx,dword ptr [ebp-14h] 004012C2 push ecx ... 18: printf("%d\n", nArray[1]); 004012D0 mov edx,dword ptr [ebp-10h] 004012D3 push edx ...为了节省篇幅,上面的汇编代码只截取了部分有代表性的内容,从上面的部分可以看到,数组采用连续的存储方式,在内存中从低地址部分到高地址部分依次存储,而普通的局部变量则是先定义的在高地址部分。在使用上也都是采用寄存器间接寻址的方式。在初始化时数组是从第0项开始依次向后赋值。但是如果我们将所有的数组成员都赋值为相同值时会怎样?9: int nArray[5] = {1}; 00401268 mov dword ptr [ebp-14h],1 0040126F xor eax,eax 00401271 mov dword ptr [ebp-10h],eax 00401274 mov dword ptr [ebp-0Ch],eax 00401277 mov dword ptr [ebp-8],eax 0040127A mov dword ptr [ebp-4],eax从上面的汇编代码可以看到,当初始化的值相同的时候,仍是采用依次赋值的方式。下面再来看看字符数组的初始化。0040126E mov eax,[string "Hello World!" (0042e01c)] 00401273 mov dword ptr [ebp-100h],eax 00401279 mov ecx,dword ptr [string "Hello World!"+4 (0042e020)] 0040127F mov dword ptr [ebp-0FCh],ecx 00401285 mov edx,dword ptr [string "Hello World!"+8 (0042e024)] 0040128B mov dword ptr [ebp-0F8h],edx 00401291 mov al,[string "Hello World!"+0Ch (0042e028)] 00401296 mov byte ptr [ebp-0F4h],al 0040129C mov ecx,3Ch 004012A1 xor eax,eax 004012A3 lea edi,[ebp-0F3h] 004012A9 rep stos dword ptr [edi] 004012AB stos word ptr [edi] 10: char *pszBuf = "Hello World!"; 004012AD mov dword ptr [ebp-104h],offset string "Hello World!" (0042e01c)字符串是特殊的字符数组,约定字符串的最后一个值为NULL。上面的代码显示出,对于字符串的初始化采用的是用寄存器的方式依次赋值4个字节的内容,而对于字符指针,在初始化的时候在程序的全局变量中存储了一个字符串,并将这个字符串的首地址赋值给对应的变量,这个字符串是位于常量内存区,所以只能寻址,而不能更改它。数组作为函数的参数当数组作为函数参数时传递的是数组的首地址,而不会拷贝整个内存区,这点许多人容易搞错。下面通过反汇编的方式来说明:void ShowArray(int a[5]) { for (int i = 0; i < 5; i++) { printf("%d\n", a[i]); } } int main() { int nArray[5] = {1, 2, 3, 4, 5}; ShowArray(nArray); return 0; }19: ShowArray(nArray); 004012FB lea eax,[ebp-14h];取[ebp - 14h]的地址 004012FE push eax 004012FF call @ILT+0(ShowArray) (00401005) 00401304 add esp,4 ;ShowArray函数 00401268 mov dword ptr [ebp-4],0;初始化i = 0 0040126F jmp ShowArray+2Ah (0040127a) 00401271 mov eax,dword ptr [ebp-4] 00401274 add eax,1 00401277 mov dword ptr [ebp-4],eax 0040127A cmp dword ptr [ebp-4],5 ;比较 i 与 5 0040127E jge ShowArray+49h (00401299);当i >= 5时跳出循环 11: { 12: printf("%d\n", a[i]); 00401280 mov ecx,dword ptr [ebp-4] ;ecx = i 00401283 mov edx,dword ptr [ebp+8] ;edx = 数组的首地址 00401286 mov eax,dword ptr [edx+ecx*4];寻址数组中的第i个元素 00401289 push eax 从上面的反汇编代码可以看出,在传值时只是将数组的首地址作为参数传入,而在函数的使用中直接通过传入的首地址来寻址数组中的各个元素,如果再函数的代码中添加一句sizeof来求这个数组的长度,那么返回的一定是4,而不是20。由于数组作为函数参数时函数不会记录数组的长度,那么为了防止越界,需要通过某种方式告知函数内部数组的长度,一般有两种方式,一种是想字符串那样规定一个结束标记,当到达这个结束标记时不再访问其下一个元素,二是通过传入一个参数表示数组的长度。另外数组作为返回值时与数组作为参数相同,都是通过指针的方式返回,但是需要牢记的一点是不要返回局部变量的地址或者引用。数组的成员的访问方式数组成员可以采用下标访问方式,也可以采用指针寻址方式,指针寻址不仅没有下标寻址方便,效率也没有下标寻址方式高。下面来看这两种方式的具体差距。11: int nArray[5] = {1, 2, 3, 4, 5}; 00401268 mov dword ptr [ebp-14h],1 ... 12: int *p = nArray; 0040128B lea eax,[ebp-14h] 0040128E mov dword ptr [ebp-18h],eax 13: printf("%d\n", nArray[3]); 00401291 mov ecx,dword ptr [ebp-8] 00401294 push ecx ... 14: printf("%d\n", p + 3); 004012A2 mov edx,dword ptr [ebp-18h] 004012A5 add edx,0Ch 004012A8 push edx从上面的代码可以看出,指针寻址会另外开辟一个4字节的内存空间用来存储这个指针变量,同时使用指针也需要进行地址变换,首先通过指针p的地址找到p的值,然后通过p存储的值再次间接寻址找到对应的值。而数组下标法寻址,只通过直接寻址找到对应的元素并取出即可。如果下标中是整型变量,则直接通过公式addr + sizeof(type) * n(其中addr为数组的首地址,type为数组元素的值,n为下标值)来寻址,而下标为整型表达式,则先计算表达式的值,然后在通过这一公式来寻址。多维数组多维数组,我们主要来说明二维数组11: int nArray[2][3] = {{1, 2, 3}, {4, 5, 6}}; 00401268 mov dword ptr [ebp-18h],1 0040126F mov dword ptr [ebp-14h],2 00401276 mov dword ptr [ebp-10h],3 0040127D mov dword ptr [ebp-0Ch],4 00401284 mov dword ptr [ebp-8],5 0040128B mov dword ptr [ebp-4],6通过汇编代码,对于多维数组在内存中存储的方式仍然为线性存储方式,对于多维数组会转化为一维数组数组,然后再依次存储各个一维数组的值,例如上面的例子中将二维数组转化为两个一维数组,然后分别在内存中对它们进行初始化。对于多维数组的寻址,例如int nArray2这样的数组,首先拆分为2个有3个元素的一维数组,在寻址时首先找到对应的一维数组的首地址,然后在对应的一维数组中寻址找到对应元素的值。这样对于多维数组都是转化为多个低一级的多维数组最终转化为一维数组的方式来解决。虽说多维数组是采用线性存储的方式来存储数据,但是在理解上我们可以将高维数组看成存储多个低维数组的特殊一维数组,比如int a4 可以看成一个有四个元素的一维数组,每一一维数组都存储了一个5个整型元素的一维数组,通过图来表示就是这样:上述的数组看做一个一维数组,这个一维数组有4个成员,每个成员都存储了一个5个一维数组的数组名,这样就可以很好的理解a 表示的是二维数组的首地址,而a[0]则表示的是第一个元素的首地址,同时也可以很好理解为何定义二维数组的指针时为何需要第二个下标,因为二维数组存储的是一维数组,它的类型就是多个一维数组,所以需要将一维数组的大小作为类型值来定义指针。函数指针函数指针的定义格式如下type (*pname)(args);函数的内容存储在代码段中,函数指针指向的就是函数的第一句代码所在的内存位置,而在调用函数需要知道函数的返回值,以及函数的参数列表,特别是参数列表,只有知道这些信息,在通过函数指针调用时才能知道其栈环境是如何配置的,函数类型其实是函数的返回值加上其参数列表,所以在定义函数时需要知道这些信息。
2016年04月26日
4 阅读
0 评论
0 点赞
2016-04-25
C语言中不同变量的访问方式
C语言中的变量大致可以分为全局变量,局部变量,堆变量和静态局部变量,这些不同的变量存储在不同的位置,有不同的生命周期。一般程序将内存分为数据段、代码段、栈段、堆段,这几类变量存储在不同的段中,造成了它们有不同的生命周期。全局变量全局变量的生命周期是整个程序的生命周期,随着程序的运行而存在,随着程序的结束而消亡,全局变量位于程序的数据段。每个应用程序有4GB的虚拟地址空间,在程序开始时系统将这个程序加载到内存中,为其分配内存,这个时候,会根据程序文件的内容,为全局变量分配内存,并为之进行初始化,当程序的生命周期结束时,系统回收进程所消耗的资源,这个时候,全局变量所占的内存被销毁。下面来看一段具体的代码:int i= 0; int main(int argc, char* argv[]) { printf("%d\n", i); return 0; }11: printf("%d\n", i); 00401268 mov eax,[i (00432e24)] 0040126D push eax 0040126E push offset string "%d\n" (0042e01c)从上述的汇编代码中可以看到,i所对应的地址为0x00432e24,在调用全局变量时,使用的是一个具体的地址,但是并没有看对应初始化i变量的反汇编代码,这是因为在程序开始运行之前,在准备进程环境的时候就为i分配的了存储空间,并进行了初始化。另外在使用时采用的是直接寻址的方式,并没有用寄存器来进行间接寻址,从这点上来看,i变量的地址不会随着程序的运行而改变,这个地址一直可以使用,所以全局变量的生命周期与程序的生命周期相同。静态变量静态变量有两个作用,一是将变量名所能使用的区域限定在对应位置,比如我们在一个函数中定义了一个静态变量,那么久只能在这个函数中使用这个变量,二是静态变量的生命周期是全局的,不会随着堆栈环境的改变而改变,下面是一个简单的例子int Func() { static int i = 0; i++; return i; } int main() { printf("%d\n", Func()); printf("%d\n", Func()); return 0; }9: static int i = 0; 10: i++; 00401268 mov eax,[_Ios_init+3 (00433e24)] 0040126D add eax,1 00401270 mov [_Ios_init+3 (00433e24)],eax 11: return i; 上面的汇编代码也采用的是直接寻址的方式,而这个静态变量的地址为0x433e24,与上面的全局变量的地址进行比较,我们可以看出,其实它也是在全局作用域的,在初始化时也没有发现有任何的初始化代码,所以我们可以说,它的生命周期也是全局的,但是由于static将其可见域限定在函数中,所以在函数外不能通过这个变量名来访问这块内存区域。局部静态变量的工作方式上面说到局部静态变量的生命周期不随函数的结束而结束,不管进入函数多少次,局部静态变量只有一个内存地址,而且只初始化一次,具体编译器是如何做到的,将用下面这一段代码来说明:int test(int n) { static int i = n; return i; } int main(int argc, char* argv[]) { for (int i = 0; i < 5; i++) { printf("%d\n", test(i)); } return 0; }12: static int i = n; 00401268 xor eax,eax 0040126A mov al,[`test'::`2'::$S25 (00433e24)];用一个字节存储了一个标志位 0040126F and eax,1 00401272 test eax,eax 00401274 jne test+3Eh (0040128e);当该标志位为1则表明进行了初始化,直接跳过初始化的步骤 00401276 mov cl,byte ptr [`test'::`2'::$S25 (00433e24)] 0040127C or cl,1;没有进行初始化的话,先初始化然后将标志位赋值为1 0040127F mov byte ptr [`test'::`2'::$S25 (00433e24)],cl 00401285 mov edx,dword ptr [ebp+8] 00401288 mov dword ptr [__pInconsistency+39Ch (00433e20)],edx 13: return i; 0040128E mov eax,[__pInconsistency+39Ch (00433e20)]在上面这段代码中我们企图多次对静态变量进行初始化,但是通过运行程序最终得到的结果都是一样的,上述的代码并没有改变静态变量的值,通过查看汇编代码我们可以看到,编译器在处理局部静态变量时多用了一个字节的内存保存了一个标志位,当该静态变量进行了初始化的时候,就跳过初始化的代码,否则进行初始化并将标志位赋相应的值。局部变量局部变量,的生命周期随着函数的调用而存在,当函数结束时它的生命周期就结束了。在我的上一篇将函数的博客中,已经说明了它寻址方式和生命周期。在函数调用时,会首先根据函数中局部变量所占的空间,初始化栈环境,并对这些局部变量进行初始化,当函数调用完成后,会首先回收栈环境,这样局部变量所在的内存被回收,用于下一个函数调用或者用作其他用途,因为栈是动态变化的,为了防止使用不当造成程序错误,所以在函数外是不能使用函数中定义的局部变量。另外一个需要说明的就是在语句块内的局部变量,它的生命周期只在语句块中,但是真实的情况是,它所在的内存与局部变量相同,都是在函数栈中,它的生命周期只在语法层面上进行限制。堆变量堆变量需要程序员自己申请并释放,需要程序员自己管理,程序不会自动管理这些内存,当调用malloc或者new 的时候,系统分配一块内存,直到调用free 或者delete的时候才释放。
2016年04月25日
6 阅读
0 评论
0 点赞
2016-04-22
C函数原理
C语言作为面向过程的语言,函数是其中最重要的部分,同时函数也是C种的一个难点,这篇文章希望通过汇编的方式说明函数的实现原理。栈结构与相关的寄存器在计算中,栈是十分重要的一种数据结构,同时也是CPU直接支持的一种数据结构,栈采用先进后出的方式。CPU中分别用两个寄存器ebp和esp来保存栈底地址和栈顶地址,在CPU层面只需要ebp的值大于ESP的值两个寄存器所指向的内存的中间的部分就构成了一个栈。汇编中采用push和pop两个指令来表示入栈和出栈,这两个指令后面直接跟寄存器或者内存地址,表示将相应的值放入栈中,比如push eax相当于指令sub esp, 4; mov [esp], eax而pop eax相当于mov [esp], eax; add esp, 4。另外CPU中有一个专门记录下一条指令的寄存器eip,这样每当执行一条指令,eip寄存器加上相应指令的长度,这样每一条指令执行完成后,eip都执向下一条指令的地址。只要能够保存函数调用前,下一句代码的地址,这样在函数执行完成后将这个地址赋值给eip寄存器,就能够回到调用者的位置,这是函数实现的基本依据。函数的调用我们通过这样一段代码来说明函数的调用过程int add(int a, int b) { int c = a + b; return c; } int main(int argc, char* argv[]) { add(1, 2); return 0; }它对应的反汇编代码如下:;这是调用函数之前所做的准备,代码在main函数中 004012A8 push 2 004012AA push 1 004012AC call @ILT+0(add) (00401005) 004012B1 add esp,8 ;函数中的汇编代码 00401250 push ebp 00401251 mov ebp,esp 00401253 sub esp,44h 00401256 push ebx 00401257 push esi 00401258 push edi 00401259 lea edi,[ebp-44h] 0040125C mov ecx,11h 00401261 mov eax,0CCCCCCCCh 00401266 rep stos dword ptr [edi] ;后面的就是函数中的实现代码首先在调用函数之前进行参数压栈,首先将参数列表中的参数从右至左,依次压栈,然后调用一句call指令,跳转到函数代码处,call指令主要有两个作用,一个是eip的值压入栈中,然后使用jmp指令,跳转到对应函数的实现位置,此时栈中的值如下图:在函数实现的位置,首先将ebp压栈,这个时候的ebp保存的是调用者的栈帧的栈底地址。然后将ESP赋值给ebp,这些指令执行后栈中的内容如下图所示:此时ebp与ESP相等,ebp上面的部分都是该函数的函数栈帧,用于保存该函数的局部变量。接下来将ESP的值减去44h,并对ESP和ebp之间的内存进行初始化为0xcc,而0xcc转化为字符串就是一系列的“烫”,还记得以前在vc6.0中写程序时经常出现的“烫烫烫”吗。这些指令就是初始化一个栈空间,这个空间大小为48h,以后在函数中定义变量时是利用ebp来做偏移,ESP因为是栈顶指针会一直变化,所以采用了一个不变的栈底指针作为偏移的基址。比如下面是add函数的语句对应的代码:10: int c = a + b; 00401268 mov eax,dword ptr [ebp+8] 0040126B add eax,dword ptr [ebp+0Ch] 0040126E mov dword ptr [ebp-4],eax初始化变量C的时候,变量的地址是ebp - 4,而从上面的图中可以看出ebp + 8指向的是第一个参数,ebp + 4指向的是保存的EIP的值。现在我们来证实一下,通过VC6.0的调试功能,查看寄存器的值,此时我们得到如下的图:在图中明显的看出此时ebp的值为0x0012FEEC,而ebp + 4则是0x0012FEF0,这个地址对应的位置存储的值为0x004012B1,看到了吗,这个地址对应的代码是不是add esp, 8;这句话是不是在call之后。当函数返回时执行下面的语句:00401271 mov eax,dword ptr [ebp-4] 12: } 00401274 pop edi 00401275 pop esi 00401276 pop ebx 00401277 mov esp,ebp 00401279 pop ebp 0040127A ret 当我们执行完了这些代码,函数栈的环境已经形成了,下面是整个栈帧环境的示意图:首先还原之前保存的寄存器环境,这几个寄存器没有太大的作用,只是编译器判断以后可能使用到它们,因而将其之前的值保存,但是与函数的实现没有太大关系,在这并不关心。之前在进入函数时首先将esp指向的位置抬高了44h,但是在这并没有看到将esp指向的位置降低44h的操作,但是它有一句mov esp, ebp,这句话就是用来还原栈环境的,还记得之前的图吗,ebp指向当前函数栈帧的栈底,通过这一句可以直接将esp还原,使其指向正确的位置。想想我当初学8086汇编利用栈操作时,不知道这个寄存器,当时压入的数据与弹出的数据不匹配结果还原到了错误的地址执行代码,可是花了好多时间调试才发现,甚是苦逼。现在好了,利用一句话直接将esp指向正确的位置,减少了不少工作,不必去记你到底压入了多少内容,也不必刻意的去将这些内容弹出。到这,栈环境又回到了当初图2的情景。然后进行了一句pop ebp将之前存储的ebp的内容还原,这个时候ebp指向的是调用者的函数栈帧的栈底位置。 在上述的最后有一句ret,相当于先执行pop eip,将之前保存的eip的值还原,这样CPU执行的下句代码就是eip指向的内存位置的代码。函数中的参数传递从上面的代码中可以看出,函数的形参与实参并不是同一个变量,它们所在的内存地址不同,这样就解释了为什么形参的改变无法影响实参,只有通过传入地址才能改变实参。我们这传递的是具体的变量值,现在我们不这么做,当传递一个结构体的话会怎么样?下面是一段测试代码:struct NUM { int a; int b; }; int add(NUM num) { int c = num.a + num.b; return c; } int main(int argc, char* argv[]) { NUM num; num.a = 1; num.b = 2; add(num); return 0; }下面是它的反汇编代码:23: num.a = 1; 004012A8 mov dword ptr [ebp-8],1 24: num.b = 2; 004012AF mov dword ptr [ebp-4],2 25: add(num); 004012B6 mov eax,dword ptr [ebp-4] 004012B9 push eax 004012BA mov ecx,dword ptr [ebp-8] 004012BD push ecx 004012BE call @ILT+0(add) (00401005)从汇编代码中可以看到,结构体在丁一时,它里面的成员是从低地址到高地址依次定义的。ebp - 8是 成员a的地址,ebp - 4是成员b的地址,在传参时,首先压入栈中的是ebp - 4 然后是ebp - 8。这样在函数栈中仍然保持着定义时候的顺序,这么做与C在底层对结构体的处理有关。其实对于参数大于4个字节的情况,一般是采用拷贝的方式,将参数所在内存中的内容依次拷贝到函数栈中。只是例子中的结构体只有两个整形成员,因此采用的是两次入栈的操作。比如我们在上面例子的结构体中添加一个char szBuf[255]的成员,这个时候在传参时会执行这样的语句:004012C2 sub esp,108h 004012C8 mov ecx,42h 004012CD lea esi,[ebp-108h] 004012D3 mov edi,esp 004012D5 rep movs dword ptr [edi],dword ptr [esi] 004012D7 call @ILT+0(add) (00401005) 004012DC add esp,108h rep movs dword ptr [edi], dword ptr [esi]指令是将esi所指向的内存依次复制到edi所指向的内存中,赋值的大小是ecx个字节,而每次赋值dword也就是4个字节。函数的返回值函数可以返回不同的值,一般利用return语句返回,但是在上面的说明中并没有这样的指令,唯一用来返回的ret指令,只是修改栈的内容并做一个跳转,并没有实际的返回什么,下面我们就来看看函数是如何返回值的。我们用第一段C代码来说明函数是如何返回的,下面是add函数和main函数的return语句对应的反汇编代码:;main函数的反汇编代码 17: return 0; 004012B4 xor eax,eax ;函数add的反汇编代码 00401268 mov eax,dword ptr [ebp+8] 0040126B add eax,dword ptr [ebp+0Ch] 0040126E mov dword ptr [ebp-4],eax 11: return c; 00401271 mov eax,dword ptr [ebp-4] ;对于返回值的使用 16: int c = add(1, 2); 004012A8 push 2 004012AA push 1 004012AC call @ILT+0(add) (00401005) 004012B1 add esp,8 004012B4 mov dword ptr [ebp-4],eax 17: return 0; 004012B7 xor eax,eax在main的返回值中,首先执行的是xor eax, eax将eax清零,然后调用ret,在add函数中,将实参相加的结果保存到eax中,然后返回,这样我们猜测函数可能通过eax来保存函数的返回值。同时在main函数中我们将返回值保存到另一个变量中,int c = add(1, 2)的反汇编代码可以看出,最终是执行了mov [ebp - 4], eax。所以从这可以看出函数如果返回四个字节的内容时会用eax保存这个返回值。如果小于4个呢,下面一段反汇编代码说明了这一点16: short c = add(1, 2); 004012A8 push 2 004012AA push 1 004012AC call @ILT+10(add) (0040100f) 004012B1 add esp,8 004012B4 mov word ptr [ebp-4],ax这段代码说明当小于4个字节时仍然会使用eax寄存器的低位存储返回值。如果大于4个字节该如何处理?struct NUM { int a; char szBuf[255]; }; NUM Ret(NUM num) { return num; } int main(int argc, char* argv[]) { NUM num = {0}; NUM num1 = Ret(num); return 0; }对应的反汇编代码如下:;main 函数的返回值部分 004012EE mov esi,eax 004012F0 mov ecx,41h 004012F5 lea edi,[ebp-30Ch] 004012FB rep movs dword ptr [edi],dword ptr [esi] 004012FD mov ecx,41h 00401302 lea esi,[ebp-30Ch] 00401308 lea edi,[ebp-208h] 0040130E rep movs dword ptr [edi],dword ptr [esi] ;Ret函数的返回值部分 16: return num; 00401268 mov ecx,41h 0040126D lea esi,[ebp+0Ch] 00401270 mov edi,dword ptr [ebp+8] 00401273 rep movs dword ptr [edi],dword ptr [esi] 00401275 mov eax,dword ptr [ebp+8] 17: } 当返回值大于4个字节时会采用其他模式,这个时候不再采用寄存器作为中间通道传递返回值,而是直接通过内存拷贝的方式来进行参数传递,在返回时,进行了内存拷贝将返回值拷贝到ebp + 8的位置,并将这个的首地址赋值给eax,使用这个值时,利用eax找到返回值所在内存的首地址,然后将这段内存的内容拷贝到相关变量所在的内存中,从在还看出了一个问题,就是返回值所在的内存的首地址为ebp + 8,如果没有保存这个值,并立即调用下一个函数的话,ebp + 8所在位置就会变成下一个函数的函数栈,这样这个返回值就丢失了,并且这个eax寄存器也会被下一个函数的返回值给覆盖,所以在调用函数后,如果不保存这个返回值,返回值就会丢失,也不能被引用。另外从上面可以看出,当参数或者返回值大于4个字节时,都要经历内存的拷贝,这样会大大降低效率,所以在参数或者返回值大于4个字节时一般利用指针或者引用来传值,如果不想函数改变出入或者传出的值,可以使用const关键字。局部变量的作用域讨论局部变量的作用域,首先来看局部变量在函数中是如何存储的。还是来看看上面的例子中的一段汇编代码10: int c = a + b; 00401268 mov eax,dword ptr [ebp+8] 0040126B add eax,dword ptr [ebp+0Ch] 0040126E mov dword ptr [ebp-4],eax在函数中定义了一个局部变量C,在反汇编代码中,可以看出C变量所在的地址为ebp -4 的位置,根据上面的图3,可以看到,这个变量是在函数栈中,在函数中使用ebp间接寻址的方式来访问,在上面的分析中编译器预留了44h的空间用来保存局部变量。在编译时编译器会计算在函数中定义的局部变量所占内存的大小,根据这个大小来为函数分配合适的栈也就是说这个时候不在是sub esp, 44h了而是根据具体需要多大的空间来抬高esp,这个就不用例子演示了,感兴趣的朋友可以写一个简单的例子来验证一下。当函数调用完成后,ebp还原到调用者的栈底部,这个时候不可能再使用ebp间接寻址的方式来找到在上一个函数中定义的局部变量了,及时我们及时保存了这个变量的地址,也有可能在调用下一个函数时,这个地址所在的内存变成了下一个函数的函数栈,被下一个函数的内容所替代。所以C中局部变量只在本函数中使用。至于在复合语句块中定义的局部变量出了这个复合语句块就不能使用,这个纯粹是语法上面的限制,其实这个时候还是可以利用ebp间接寻址的方式来访问。函数的三种调用约定我们知道函数中十分重要的一个部分是对栈空间的使用和最后栈空间的回收,不同的函数类型有不同的参数压栈与栈空间还原的方式,具体使用哪一种方式,需要事先与编译器约定好,以便生成对应的机器码来处理。下面我们来探究这三种调用方式。stdcall方式void _stdcall Print(int i, int k) { int j = 0; printf("i = %d\n, k = %d\n", i, k); } int main(int argc, char* argv[]) { Print(10, 20); return 0; }下面是对应的反汇编代码;main函数中的反汇编代码 16: Print(10, 20); 004012C8 push 14h 004012CA push 0Ah 004012CC call @ILT+0(Print) (00401005) ;Print函数中反汇编代码 00401268 mov dword ptr [ebp-4],0 11: printf("i = %d\n, k = %d\n", i, k); 0040126F mov eax,dword ptr [ebp+0Ch] 00401272 push eax 00401273 mov ecx,dword ptr [ebp+8] 00401276 push ecx 00401277 push offset string "i = %d\n, k = %d\n" (0042f01c) 0040127C call printf (00401570) 00401281 add esp,0Ch 12: } 00401284 pop edi 00401285 pop esi 00401286 pop ebx 00401287 add esp,44h 0040128A cmp ebp,esp 0040128C call __chkesp (00401410) 00401291 mov esp,ebp 00401293 pop ebp 00401294 ret 8从上面的代码中可以看出在调用函数Print函数时,首先压入栈中的参数是0x14,然后是0x0A,这两个值对应的是20和10,也就是说这种调用方式参数采用的是从右至左压栈,然后我们看到在函数栈环境的初始化中,与之前所说的基本相同,在返回时有一句ret 8这句话是相当于先执行了ret,然后执行了add esp , 8的操作,在调用这句话之前,esp保存的是该函数栈底的指针,esp + 8 正好跳过了之前为形参准备的栈空间,也就是说这种调用方式是由被调函数本身来完成最后栈空间的回收工作。cdecl方式这种方式是C/C++默认的函数调用方式。我们将上述代码中的_stdcall改为 _cdecl,下面是函数的部分反汇编代码:;main部分 16: Print(10, 20); 004012C8 push 14h 004012CA push 0Ah 004012CC call @ILT+0(Print) (00401005) 004012D1 add esp,8 ;print函数部分 0040128C call __chkesp (00401410) 00401291 mov esp,ebp 00401293 pop ebp 00401294 ret函数栈的初始化工作的代码基本相同,这里就不再粘贴这段代码了。首先在调用这个函数时压栈方式也是从右至左一次压栈,但是函数调用完毕,返回时只要一句ret,而在main函数中多了一句add esp, 8从这个地方可以很明显的看出,最后参数所在空间的释放是由main函数释放,也就是函数栈的释放是由调用方来完成。还记得在Windows SDK程序中的WinMain函数前面的WINAPI吗,其实它是一个宏,表示的正式这种调用方式。fastcallfastcall是采用一种特殊的方式调用,一般函数的做法是将参数压入函数栈中,采用的是内存拷贝的方式,而这种方式为了体现fast的特性,部分参数是用寄存器来传值,我们知道寄存器的存取速度是大于内存的,所以这种方式也就可以提高程序的运行效率,但是寄存器数量是有限的,因此这种方式是采用寄存器与内存混合使用的方式来传递参数。void _fastcall Print(int i, int k, int a, int b) { int j = 0; printf("i = %d\n, k = %d, a = %d, b = %d\n", i, k, a, b); } int main(int argc, char* argv[]) { Print(10, 20, 30, 40); return 0; }对应的反汇编代码如下:;main函数的部分 16: Print(10, 20, 30, 40); 004012D8 push 28h;栈内存传参 004012DA push 1Eh 004012DC mov edx,14h;寄存器传参 004012E1 mov ecx,0Ah 004012E6 call @ILT+0(Print) (00401005) 17: return 0; ;print函数返回部分 00401294 pop edi 00401295 pop esi 00401296 pop ebx 00401297 add esp,4Ch 0040129A cmp ebp,esp 0040129C call __chkesp (00401420) 004012A1 mov esp,ebp 004012A3 pop ebp 004012A4 ret 8 ;平衡函数栈帧从上面的反汇编代码可以看出,这种调用方式是采用寄存器与函数栈混合传参的方式,在返回时,由函数本身平衡栈帧。不定参函数在函数中,可以使用这样一种技术:传入的参数个数可变,,比如像printf和scan,这种函数至少需要一个参数,并且需要知道参数个数,和各个参数类型,比如printf传入一个格式字符串来表示参数个数和参数的类型。从上面所说的函数的原理来看,参数是从右至左压栈,这样只需要知道第一个参数的地址,就可以依次向下寻找到各个参数的地址,通过各个参数的类型向下寻址,比如当前参数类型是int型,那么它的下一个参数的地址就是这个地址加4的位置,同时为了防止越界访问,给出了参数个数。下面我们用一个简单的例子来说明如何使用这种方式寻址。//规定函数的第一个参数表示后续参数的个数,后面的参数全为int void Print(int nCout,...) { int *p = &nCout; for (int i = 0; i < nCout;i++) { p++; printf("%d\t", *p); } } int main(int argc, char* argv[]) { Print(3, 20, 30, 40); return 0; }我们知道参数列表中的参数都是从右至左依次压入函数栈中,所以这些参数肯定是依次存放,且第一个参数所在的地址应该是最小的,以后只需要依次根据将指针向下偏移即可寻址到不同的参数,C语言为了简化这个操作,定义了一组宏va_list va_start va_arg va_end。这组宏的实现原理其实与上面我们写的代码差不多。由于传递的参数个数不确定,所以这个函数本身并不知道有多少个参数会传入,所以希望函数本身来平衡函数栈是不可能的,只有在调用之时才知道这个参数的个数,所以平衡栈的工作只能是由调用者来做,所以上述三种方式只有_cdecl这种方式可以使用不定参函数。最后我们来总结一下函数的调用一般经过如下步骤:首先从右至左将参数压入栈中然后调用call指令保存eip寄存器的值,然后跳转到函数代码将上一个函数的栈底地址ebp的值压入栈中将此时esp的值保存到ebp中,作为该函数的函数栈的栈底地址根据函数中局部变量的个数抬高esp的值并初始化这段栈空间将其余寄存器的值压栈执行函数代码通过eax或者内存拷贝的方式保存返回值将上面保存的寄存器的值出栈执行esp = ebp,时esp指向函数栈的栈底pop ebp 还原之前保存的值,使ebp指向调用者的函数栈栈底ret 返回或者ret n(n为整数)指令返回到调用者的下一句代码平衡堆栈(根据约定方式决定是否有这步)
2016年04月22日
5 阅读
0 评论
0 点赞
2016-04-11
C语言循环的实现
在C语言中采用3中语法来实现循环,它们分别是while、for、do while,本文将分别说明这三种循环的实现,并对它们的运行效率进行比较。do while首先来看do while的实现:下面是简单的代码:int nCount = 0; int nMax = 10; do { nCount++; } while (nCount < nMax); return 0; 下面对应的是它的汇编代码:9: int nCount = 0; 00401268 mov dword ptr [ebp-4],0 10: int nMax = 10; 0040126F mov dword ptr [ebp-8],0Ah 11: do 12: { 13: nCount++; 00401276 mov eax,dword ptr [ebp-4] 00401279 add eax,1 0040127C mov dword ptr [ebp-4],eax 14: } while (nCount < nMax); 0040127F mov ecx,dword ptr [ebp-4];exc = nCount 00401282 cmp ecx,dword ptr [ebp-8];比较nCount 和 nMax的值 00401285 jl main+26h (00401276);跳转到循环体中 15: return 0; 00401287 xor eax,eax在汇编代码中首先执行了一次循环体中的操作,然后判断,当条件满足时会跳转回循环体,然后再次执行,当条件不满足时会接着执行后面的语句。这个过程可以用goto来模拟: int nCount = 0; int nMax = 10; __WHILE: nCount++; if(nCount < nMax) goto __WHILE;while循环不同于do while的先执行再比较,while采取的是先比较再循环的方式,下面是一个while的例子: int nCount = 0; int nMax = 10; while (nCount < nMax) { nCount++; }00401268 mov dword ptr [ebp-4],0 10: int nMax = 10; 0040126F mov dword ptr [ebp-8],0Ah 11: while (nCount < nMax) 00401276 mov eax,dword ptr [ebp-4] 00401279 cmp eax,dword ptr [ebp-8] 0040127C jge main+39h (00401289) 12: { 13: nCount++; 0040127E mov ecx,dword ptr [ebp-4] 00401281 add ecx,1 00401284 mov dword ptr [ebp-4],ecx 14: } 00401287 jmp main+26h (00401276) 15: return 0; 00401289 xor eax,eax 从汇编代码上可以看出,执行while循环时会有两次跳转,当条件不满足时会执行一次跳转,跳转到循环体外,而条件满足,执行完一次循环后,会再次跳转到循环体中,再次进行比较。相比于do while来说,while执行了两次跳转,效率相对较低。for 循环for循环是首先进行初始化操作然后进行比较,条件满足时执行循环,再将循环变量递增,最后再次比较,执行循环或者跳出。下面是for的简单例子: int nMax = 10; for (int i = 0; i < nMax; i++) { printf("%d\n", i); }下面是它对应的汇编代码:9: int nMax = 10; 00401268 mov dword ptr [ebp-4],0Ah 10: for (int i = 0; i < nMax; i++) 0040126F mov dword ptr [ebp-8],0 ;初始化循环变量 00401276 jmp main+31h (00401281);跳转到比较操作处 00401278 mov eax,dword ptr [ebp-8] 0040127B add eax,1 0040127E mov dword ptr [ebp-8],eax;这三句话实现的是循环变量自增操作 00401281 mov ecx,dword ptr [ebp-8];ecx = i 00401284 cmp ecx,dword ptr [ebp-4];比较ecx与i 00401287 jge main+4Ch (0040129c);跳转到循环体外 11: { 12: printf("%d\n", i); 00401289 mov edx,dword ptr [ebp-8] 0040128C push edx 0040128D push offset string "%d\n" (0042e01c) 00401292 call printf (00401540) 00401297 add esp,8 13: } 0040129A jmp main+28h (00401278);跳转到i++位置 14: return 0; 0040129C xor eax,eax从上面的汇编代码可以看出for循环的效率最低,它经过了3次跳转,生成对应的汇编代码上,初始化操作后面紧接着是循环变量自增操作,所以首先在完成初始化后会进行一次跳转,跳转到判断,然后根据判断条件再次跳转或者接着执行循环体,最后当循环完成后会再次跳转到循环变量自增的位置,同样采用goto语句来模拟这个操作: int nMax = 10; int i = 0; goto __CMP; __ADD: i++; __CMP: if (i >= nMax) { goto __RETURN; } __LOOP: printf("%d\n", i); goto __ADD; __RETURN: return 0;continue语句continue用于结束这次循环进入下一次循环,下面采用最复杂的for循环来说明continue语句:int nMax = 10; int i = 0; for(;i < nMax; i++) { if (i == 6) { continue; } }下面是它对应的汇编代码:00401268 mov dword ptr [ebp-4],0Ah 10: int i = 0; 0040126F mov dword ptr [ebp-8],0 11: for(;i < nMax; i++) 00401276 jmp main+31h (00401281) 00401278 mov eax,dword ptr [ebp-8] 0040127B add eax,1 0040127E mov dword ptr [ebp-8],eax 00401281 mov ecx,dword ptr [ebp-8] 00401284 cmp ecx,dword ptr [ebp-4] 00401287 jge main+43h (00401293) 12: { 13: if (i == 6) 00401289 cmp dword ptr [ebp-8],6; 0040128D jne main+41h (00401291);条件不满足组跳转到循环结束处 14: { 15: continue; 0040128F jmp main+28h (00401278) 16: } 17: } 00401291 jmp main+28h (00401278) 18: return 0; 00401293 xor eax,eax 从上面的汇编代码可以看到,continue语句也是一个跳转语句,它会直接跳转到循环体的开始位置。对于for来说相对特殊一些(我觉得循环变量自增并不属于循环体),由于第一次进入循环时并没有执行循环变量自增,所以它会跳转到循环变量自增的位置,其他则直接到循环开始处。慎用gotogoto 语句就像汇编中的 jmp 一样,是直接跳转到对应的标识位置,从上面我们使用goto来模拟各种循环来看,goto语句的可读性不强,而且有可能跳过变量的初始化等过程造成一些难以察觉的问题,但有些时候goto确实好用,例如在写socket或者其他需要清理资源的代码时,goto可以显著的增加程序的可读性并且也会减少相关代码的编写,例如一个典型的服务端socket例子#include <winsock2.h> #include <stdio.h> #include <stdlib.h> #pragma comment(lib, "ws2_32.lib") // Winsock Library #define PORT 8080 #define BUFFER_SIZE 1024 int main() { WSADATA wsaData; SOCKET serverSocket, clientSocket; struct sockaddr_in serverAddr, clientAddr; int addrLen = sizeof(clientAddr); char buffer[BUFFER_SIZE]; // 初始化 Winsock if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("Failed to initialize Winsock. Error Code: %d\n", WSAGetLastError()); return EXIT_FAILURE; } // 创建 socket serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket == INVALID_SOCKET) { printf("Could not create socket. Error Code: %d\n", WSAGetLastError()); WSACleanup(); return EXIT_FAILURE; } // 设置服务器地址结构 serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口 serverAddr.sin_port = htons(PORT); // 转换为网络字节序 // 绑定 socket if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { printf("Bind failed. Error Code: %d\n", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return EXIT_FAILURE; } // 开始监听 if (listen(serverSocket, 3) == SOCKET_ERROR) { printf("Listen failed. Error Code: %d\n", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return EXIT_FAILURE; } printf("Server is listening on port %d...\n", PORT); // 接受客户端连接 clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen); if (clientSocket == INVALID_SOCKET) { printf("Accept failed. Error Code: %d\n", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return EXIT_FAILURE; } printf("Client connected.\n"); // 发送消息给客户端 const char *message = "Hello from server!"; send(clientSocket, message, strlen(message), 0); // 关闭 sockets closesocket(clientSocket); closesocket(serverSocket); WSACleanup(); return EXIT_SUCCESS; }中间有好几次执行了closesocket、以及最后的WSACleanup操作、前面每一步出错都要写一次这些清理资源的操作。如果使用goto将会简单的多#include <winsock2.h> #include <stdio.h> #include <stdlib.h> #pragma comment(lib, "ws2_32.lib") // Winsock Library #define PORT 8080 #define BUFFER_SIZE 1024 int main() { WSADATA wsaData; SOCKET serverSocket, clientSocket; struct sockaddr_in serverAddr, clientAddr; int addrLen = sizeof(clientAddr); char buffer[BUFFER_SIZE]; int err = EXIT_SUCCESS; // 初始化 Winsock if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("Failed to initialize Winsock. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; goto __CLEANUP; } // 创建 socket serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket == INVALID_SOCKET) { printf("Could not create socket. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; goto __CLEANUP; } // 设置服务器地址结构 serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口 serverAddr.sin_port = htons(PORT); // 转换为网络字节序 // 绑定 socket if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { printf("Bind failed. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; goto __CLEANUP; } // 开始监听 if (listen(serverSocket, 3) == SOCKET_ERROR) { printf("Listen failed. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; goto __CLEANUP; } printf("Server is listening on port %d...\n", PORT); // 接受客户端连接 clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen); if (clientSocket == INVALID_SOCKET) { printf("Accept failed. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; goto __CLEANUP; } printf("Client connected.\n"); // 发送消息给客户端 const char *message = "Hello from server!"; send(clientSocket, message, strlen(message), 0); // 关闭 sockets __CLEANUP: if(clientSocket != INVALID_SOCKET) { closesocket(clientSocket) } if(serverSocket != INVALID_SOCKET) { closesocket(serverSocket); } WSACleanup(); return err; }如果在不允许使用goto的情况下,可以考虑使用 do while 来模拟这种情况,上面的代码可以修改为#include <winsock2.h> #include <stdio.h> #include <stdlib.h> #pragma comment(lib, "ws2_32.lib") // Winsock Library #define PORT 8080 #define BUFFER_SIZE 1024 int main() { WSADATA wsaData; SOCKET serverSocket, clientSocket; struct sockaddr_in serverAddr, clientAddr; int addrLen = sizeof(clientAddr); char buffer[BUFFER_SIZE]; int err = EXIT_SUCCESS; do{ // 初始化 Winsock if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("Failed to initialize Winsock. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; break; } // 创建 socket serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket == INVALID_SOCKET) { printf("Could not create socket. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; break; } // 设置服务器地址结构 serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的接口 serverAddr.sin_port = htons(PORT); // 转换为网络字节序 // 绑定 socket if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { printf("Bind failed. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; break; } // 开始监听 if (listen(serverSocket, 3) == SOCKET_ERROR) { printf("Listen failed. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; break; } printf("Server is listening on port %d...\n", PORT); // 接受客户端连接 clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &addrLen); if (clientSocket == INVALID_SOCKET) { printf("Accept failed. Error Code: %d\n", WSAGetLastError()); err = EXIT_FAILURE; break; } printf("Client connected.\n"); // 发送消息给客户端 const char *message = "Hello from server!"; send(clientSocket, message, strlen(message), 0); }while (FALSE); // 关闭 sockets if(clientSocket != INVALID_SOCKET) { closesocket(clientSocket) } if(serverSocket != INVALID_SOCKET) { closesocket(serverSocket); } WSACleanup(); return err; }这里的while不是为了循环,而是利用了do while 无论如何都会先执行循环体中代码的特性,只执行一次上述主体代码,利用break来跳转到最后的清理模块,实现与goto 类似的效果。使用goto 的方案比do while的方案要显得简洁易懂,goto使用的好,也能使得程序简单易懂。具体使用哪种方案是个见仁见智的事情,看个人喜好。如果遇上公司要求不能使用 goto,那么就可以采用do while的实现方案
2016年04月11日
16 阅读
0 评论
0 点赞
2016-04-10
IF和SWITCH的原理
在C语言中,if和switch是条件分支的重要组成部分。ifif的功能是计算判断条件的值,根据返回的值的不同来决定跳转到哪个部分。值为真则跳转到if语句块中,否则跳过if语句块。下面来分析一个简单的if实例:if(argc > 0) { printf("argc > 0\n"); } if (argc <= 0) { printf("argc <= 0\n"); } printf("argc = %d\n", argc);它对应的汇编代码如下:9: if(argc > 0) cmp dword ptr [ebp+8],0 0040102C jle main+2Bh (0040103b) ;argc <= 0就跳转到下一个if处 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (0042003c) call printf (00401090) add esp,4 12: } 13: if (argc <= 0) ;argc > 0跳转到后面的printf语句输出argc的值 0040103B cmp dword ptr [ebp+8],0 0040103F jg main+3Eh (0040104e) 14: { 15: printf("argc <= 0\n"); push offset string "argc <= 0\n" (0042002c) call printf (00401090) 0040104B add esp,4 16: } 17: printf("argc = %d\n", argc); 0040104E mov eax,dword ptr [ebp+8] push eax push offset string "argc = %d\n" (0042001c) call printf (00401090) 0040105C add esp,8根据汇编代码我们看到,首先执行第一个if中的比较,jle表示当cmp得到的结果≤0时会进行跳转,第二个if在汇编中的跳转条件是>0,从这个上面可以看出在代码执行过程当中if转换的条件判断语句与if的判断结果时相反的,也就是说cmp比较后不成立则跳转,成立则向下执行。同时每一次跳转都是到当前if语句的下一条语句。if ...else下面来看看if...else...语句的跳转。if(argc > 0) { printf("argc > 0\n"); }else { printf("argc <= 0\n"); } printf("argc = %d\n", argc);它所对应的汇编代码如下:00401028 cmp dword ptr [ebp+8],0 0040102C jle main+2Dh (0040103d) ;条件不满足则跳转到else语句块中 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (0042003c) 00401033 call printf (00401090) 00401038 add esp,4 12: }else 0040103B jmp main+3Ah (0040104a);如果执行if语句块就会执行这条语句跳出else语句块 13: { 14: printf("argc <= 0\n"); 0040103D push offset string "argc <= 0\n" (0042002c) 00401042 call printf (00401090) 00401047 add esp,4 15: } 16: printf("argc = %d\n", argc); 0040104A mov eax,dword ptr [ebp+8]上述的汇编代码指出,对于if...else..语句,首先进行条件判断,if表达式为真,则继续执行if快中的语句,然后利用jmp跳转到else语句块外,否则会利用jmp跳转到else语句块中,然后依次执行其后的每一句代码。if ... else if... else最后再来展示if...else if...else这种分支结构:if(argc > 0) { printf("argc > 0\n"); }else if(argc < 0) { printf("argc < 0\n"); }else { printf("argc == 0\n"); } printf("argc = %d\n", argc);汇编代码如下:9: if(argc > 0) 00401028 cmp dword ptr [ebp+8],0 0040102C jle main+2Dh (0040103d);条件不满足则会跳转到下一句else if中 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (00420f9c) 00401033 call printf (00401090) 00401038 add esp,4 12: }else if(argc < 0) 0040103B jmp main+4Fh (0040105f) ;当上述条件符合则执行这条语句跳出分支外,跳转的地址正是else语句外的printf语句 0040103D cmp dword ptr [ebp+8],0 00401041 jge main+42h (00401052) 13: { 14: printf("argc < 0\n"); 00401043 push offset string "argc < 0\n" (0042003c) 00401048 call printf (00401090) 0040104D add esp,4 15: }else 00401050 jmp main+4Fh (0040105f) 16: { 17: printf("argc == 0\n"); 00401052 push offset string "argc <= 0\n" (0042002c) 00401057 call printf (00401090) 0040105C add esp,4 18: } 19: printf("argc = %d\n", argc); 0040105F mov eax,dword ptr [ebp+8]通过汇编代码可以看到对于这种结构,会依次判断每个if语句中的条件,当有一个满足,执行完对应语句块中的代码后,会直接调转到分支结构外部,当前面的条件都不满足则会执行else语句块中的内容。这个逻辑结构在某些情况下可以利用if return if return 这种结构来替代。当某一条件满足时执行完对应的语句后直接返回而不执行其后的代码。一条提升效率的做法是将最有可能满足的条件放在前面进行比较,这样可以减少比较次数,提升效率。switchswitch是另一种比较常用的多分支结构,在使用上比较简单,效率上也比if...else if...else高,下面将分析switch结构的实现switch(argc) { case 1: printf("argc = 1\n"); break; case 2: printf("argc = 2\n"); break; case 3: printf("argc = 3\n"); break; case 4: printf("argc = 4\n"); break; case 5: printf("argc = 5\n"); break; case 6: printf("argc = 6\n"); break; default: printf("else\n"); break; }对应的汇编代码如下:0040B798 mov eax,dword ptr [ebp+8] ;eax = argc 0040B79B mov dword ptr [ebp-4],eax 0040B79E mov ecx,dword ptr [ebp-4] ;ecx = eax 0040B7A1 sub ecx,1 0040B7A4 mov dword ptr [ebp-4],ecx 0040B7A7 cmp dword ptr [ebp-4],5 0040B7AB ja $L544+0Fh (0040b811) ;argc 》 5则跳转到default处,至于为什么是5而不是6,看后面的说明 0040B7AD mov edx,dword ptr [ebp-4] ;edx = argc 0040B7B0 jmp dword ptr [edx*4+40B831h] 11: case 1: 12: printf("argc = 1\n"); 0040B7B7 push offset string "argc = 1\n" (00420fc0) 0040B7BC call printf (00401090) 0040B7C1 add esp,4 13: break; 0040B7C4 jmp $L544+1Ch (0040b81e) 14: case 2: 15: printf("argc = 2\n"); 0040B7C6 push offset string "argc = 2\n" (00420fb4) 0040B7CB call printf (00401090) 0040B7D0 add esp,4 16: break; 0040B7D3 jmp $L544+1Ch (0040b81e) 17: case 3: 18: printf("argc = 3\n"); 0040B7D5 push offset string "argc = 3\n" (00420fa8) 0040B7DA call printf (00401090) 0040B7DF add esp,4 19: break; 0040B7E2 jmp $L544+1Ch (0040b81e) 20: case 4: 21: printf("argc = 4\n"); 0040B7E4 push offset string "argc = 4\n" (00420f9c) 0040B7E9 call printf (00401090) 0040B7EE add esp,4 22: break; 0040B7F1 jmp $L544+1Ch (0040b81e) 23: case 5: 24: printf("argc = 5\n"); 0040B7F3 push offset string "argc < 0\n" (0042003c) 0040B7F8 call printf (00401090) 0040B7FD add esp,4 25: break; 0040B800 jmp $L544+1Ch (0040b81e) 26: case 6: 27: printf("argc = 6\n"); 0040B802 push offset string "argc <= 0\n" (0042002c) 0040B807 call printf (00401090) 0040B80C add esp,4 28: break; 0040B80F jmp $L544+1Ch (0040b81e) 29: default: 30: printf("else\n"); 0040B811 push offset string "argc = %d\n" (0042001c) 0040B816 call printf (00401090) 0040B81B add esp,4 31: break; 32: } 33: 34: return 0; 0040B81E xor eax,eax上面的代码中并没有看到像if那样,对每一个条件都进行比较,其中有一句话 “jmp dword ptr [edx*4+40B831h]” 这句话从表面上看应该是取数组中的元素,再根据元素的值来进行跳转,而这个元素在数组中的位置与eax也就是与argc的值有关,下面我们跟踪到数组中查看数组的元素值:0040B831 B7 B7 40 00 0040B835 C6 B7 40 00 0040B839 D5 B7 40 00 0040B83D E4 B7 40 00 0040B841 F3 B7 40 00 0040B845 02 B8 40 00通过对比可以发现0x0040b7b7是case 1处的地址,后面的分别是case 2、case 3、case 4、case 5、case 6处的地址,每个case中的break语句都翻译为了同一句话“jmp $L544+1Ch (0040b81e)”,所以从这可以看出,在switch中,编译器多增加了一个数组用于存储每个case对应的地址,根据switch中传入的整数在数组中查到到对应的地址,直接通过这个地址跳转到对应的位置,减少了比较操作,提升了效率。编译器在处理switch时会首先校验不满足所有case的情况,当这种情况发生时代码调转到default或者switch语句块之外。然后将传入的整数值减一(数组元素是从0开始计数)。最后根据参数值找到应该跳转的位置。上述的代码case是从0~6依次递增,这样做确实可行,但是当我们在case中的值并不是依次递增的话会怎样?此时根据不同的情况编译器会做不同的处理。一般任然会建立这样的一个表,将case中出现的值填写对应的跳转地址,没有出现的则将这个地址值填入default对应的地址或者switch语句结束的地址,比如当我们上述的代码去掉case 5, 这个时候填入的地址值如下图所示:如果每两个case之间的差距大于6,或者case语句数小于4则不会采取这种做法,如果再采用这种方式,那么会造成较大的资源消耗。这个时候编译器会采用索引表的方式来进行地址的跳转。下面有这样一个例子:switch(argc) { case 1: printf("argc = 1\n"); break; case 2: printf("argc = 2\n"); break; case 5: printf("argc = 5\n"); break; case 6: printf("argc = 6\n"); break; case 255: printf("argc = 255\n"); default: printf("else\n"); break; }它对应的汇编代码如下:0040B798 mov eax,dword ptr [ebp+8] 0040B79B mov dword ptr [ebp-4],eax 0040B79E mov ecx,dword ptr [ebp-4] ;到此eax = ecx = argc 0040B7A1 sub ecx,1 0040B7A4 mov dword ptr [ebp-4],ecx 0040B7A7 cmp dword ptr [ebp-4],0FEh 0040B7AE ja $L542+0Dh (0040b80b) ;当argc > 255则跳转到default处 0040B7B0 mov eax,dword ptr [ebp-4] 0040B7B3 xor edx,edx 0040B7B5 mov dl,byte ptr (0040b843)[eax] 0040B7BB jmp dword ptr [edx*4+40B82Bh] 11: case 1: 12: printf("argc = 1\n"); 0040B7C2 push offset string "argc = 1\n" (00420fb4) 0040B7C7 call printf (00401090) 0040B7CC add esp,4 13: break; 0040B7CF jmp $L542+1Ah (0040b818) 14: case 2: 15: printf("argc = 2\n"); 0040B7D1 push offset string "argc = 3\n" (00420fa8) 0040B7D6 call printf (00401090) 0040B7DB add esp,4 16: break; 0040B7DE jmp $L542+1Ah (0040b818) 17: case 5: 18: printf("argc = 5\n"); 0040B7E0 push offset string "argc = 5\n" (00420f9c) 0040B7E5 call printf (00401090) 0040B7EA add esp,4 19: break; 0040B7ED jmp $L542+1Ah (0040b818) 20: case 6: 21: printf("argc = 6\n"); 0040B7EF push offset string "argc < 0\n" (0042003c) 0040B7F4 call printf (00401090) 0040B7F9 add esp,4 22: break; 0040B7FC jmp $L542+1Ah (0040b818) 23: case 255: 24: printf("argc = 255\n"); 0040B7FE push offset string "argc <= 0\n" (0042002c) 0040B803 call printf (00401090) 0040B808 add esp,4 25: default: 26: printf("else\n"); 0040B80B push offset string "argc = %d\n" (0042001c) 0040B810 call printf (00401090) 0040B815 add esp,4 27: break; 28: } 29: 30: return 0; 0040B818 xor eax,eax这段代码与上述的线性表相比较区别并不大,只是多了一句 “mov dl,byte ptr (0040b843)[eax]” 这似乎又是一个数组,通过查看内存可以知道这个数组的值分别为:00 01 05 05 02 03 05 05 ... 04,下一句根据这些值在另外一个数组中查找数据,我们列出另外一个数组的值:C2 B7 40 00 D1 B7 40 00 E0 B7 40 00 EF B7 40 00 FE B7 40 00 0B B8 40 00通过对比我们发现,这些值分别是每个case与default入口处的地址,编译器先查找到每个值在数组中对应的元素位置,然后根据这个位置值再在地址表中从、找到地址进行跳转,这个过程可以用下面的图来表示:这样通过一个每个元素占一个字节的表,来表示对应的case在地址表中所对应的位置,从而跳转到对应的地址,这样通过对每个case增加一个字节的内存消耗来达到,减少地址表对应的内存消耗。在上述的汇编代码中,是利用dl寄存器来存储对应case在地址表中项,这样就会产生一个问题,当case 值大于 255,也就是超出了一个字节的,超出了dl寄存器的表示范围时,又该如何来进行跳转这个时候编译器会采用判定树的方式来进行判定,在根节点保存的是所有case值的中位数, 左子树都是大于这个大于这个值的数,右字数是小于这个值的数,通过每次的比较来得到正确的地址。比如下面的这个判定树:首先与10进行比较,根据与10 的大小关系进入左子树或者右子树,再看看左右子树的分支是否不大于3,若不大于3则直接转化为对应的if...else if... else结构,大于3则检测分支是否满足上述的优化条件,满足则进行对应的地址表或者索引表的优化,否则会再次对子树进行优化,以便减少比较次数。
2016年04月10日
6 阅读
0 评论
0 点赞
2016-02-28
C/C++中define定义的常量与const常量
常量是在程序中不能更改的量,在C/C++中有两种方式定义常量,一种是利用define宏定义的方式,一种是C++中新提出来的const型常变量,下面主要讨论它们之间的相关问题;define定义的常量:define是预处理指令的一种,它用来定义宏,宏只是一个简单的替换,将宏变量所对应的值替换,如下面的代码:#define NUM 2 int main() { printf("%d", NUM); }编译器在编译时处理的并不是这样的代码,编译器会首先处理预处理指令,根据预处理指令生成相关的代码文件,然后编译这个文件,得到相关的.obj文件,最后通过链接相关的.obj文件得到一个可执行文件,最典型的是我们一般在.cpp文件中写的#include指令,在处理时首先将所需包含的头文件整个拷贝到这个.cpp文件中,并替换这个#include指令,然后再编译生成的文件,这个中间文件在Windows中后缀为.i,在Visual C++ 6.0中以此点击Project-->Settings-->C/C++,在Project Options最后一行加上'/P'(P为大写)这样在点击编译按钮时不会编译生成obj文件,只会生成.i文件,通过这个.i文件可以看到在做预处理的时候会将 NUM替换成2然后在做编译处理,这个时候点击生成时会出错,因为我们将编译选项修改后没有生成.obj文件但是在生成时需要这个文件,因此会报错,所以在生成时要去掉这个/P选项。而我们看到在使用const 定义的时候并没有这个替换的操作,与使用正常的变量无异。const型变量只是在语法层面上限定这个变量的值不可以修改,我们可以通过强制类型转化或者通过内嵌汇编的形式修改这个变量的值,比如下面的代码:// 强制类型转化 int main(int argc, char* argv[]) { const nNum = 10; int *pNum = (int*)&nNum; printf("%d\n", nNum); return 0; }//嵌入汇编的形式 const nNum = 10; __asm { mov [ebp - 4], 10 } printf("%d\n", nNum); return 0;但是我们看到,这两种方式修改后,输出的值仍然是10,这个原因我们可以通过查看反汇编代码查看;printf("%d\n", nNum); 00401036 push 0Ah 00401038 push offset string "%d\n" (0042001c) 0040103D call printf (00401070) 00401042 add esp,8在调用printf的时候,入栈的参数是10,根本没有取nNum值得相关操作,在利用const定义的常量时,编译器认为既然这是一个常量,应该不会修改,为了提升效率,在使用时并不会去对应的内存中寻址,而是直接将它替换为初始化时的值,为了防止这种事情的发生,可以利用C++中的关键字:volatile。这个关键字保证每次在使用变量时都去内存中读取。我们可以总结出const和define的几个不同之处:define是一个预处理指令,const是一个关键字。define定义的常量编译器不会进行任何检查,const定义的常量编译器会进行类型检查,相对来说比define更安全define的宏在使用时是替换不占内存,而const则是一个变量,占内存空间define定义的宏在代码段中不可寻址,const定义的常量是可以寻址的,在数据段或者栈段中。define定义的宏在编译前的预处理操作时进行替换,而const定义变量是在编译时决定define定义的宏是真实的常量,不会被修改,const定义的实际上是一个变量,可以通过相关的手段进行修改。
2016年02月28日
6 阅读
1 评论
0 点赞
2016-01-03
地址、指针与引用
计算机本身是不认识程序中给的变量名,不管我们以何种方式给变量命名,最终都会转化为相应的地址,编译器会生成一些符号常量并且与对应的地址相关联,以达到访问变量的目的。 变量是在内存中用来存储数据以供程序使用,变量主要有两个部分构成:变量名、变量类型,其中变量名对应了一块具体的内存地址,而变量类型则表明该如何翻译内存中存储的二级制数。我们知道不同的类型翻译为二进制的值不同,比如整型是直接通过数学转化、浮点数是采用IEEE的方法、字符则根据ASCII码转化,同样变量类型决定了变量所占的内存大小,以及如何在二进制和变量所表达的真正意义之间转化。而指针变量也是一个变量,在内存中也占空间,不过比较特殊的是它存储的是其他变量的地址。在32位的机器中,每个进程能访问4GB的内存地址空间,所以程序中的地址采用32位二进制数表示,也就是一个整型变量的长度,地址值一般没有负数所以准确的说指针变量的类型应该是unsigned int 即每个指针变量占4个字节。还记得在定义结构体中可以使用该结构体的指针作为成员,但是不能使用该结构的实例作为成员吗?这是因为编译器需要根据各个成员变量的大小分配相关的内存,用该结构体的实例作为成员时,该结构体根本没有定义完整,编译器是不会知道该如何分配内存的,而任何类型的指针都只占4个字节,编译器自然知道如何分配内存。我们在书写指针变量时给定的类型是它所指向的变量的类型,这个类型决定了如何翻译所对应内存中的值,以及该访问多少个字节的内存。对指针的间接访问会先先取出值,访问到对应的内存,再根据指针所指向的变量的类型,翻译成对应的值。一般指针只能指向对应类型的变量,比如int类型的指针只能指向int型的变量,而有一种指针变量可以指向所有类型的变量,它就是void类型的指针变量,但是由于这种类型的变量没有指定它所对应的变量的类型,所以即使有了对应的地址,它也不知道该取多大内存的数据,以及如何解释这些数据,所以这种类型的指针不支持间接访问,下面是一个间接访问的例子:int main() { int nValue = 10; float fValue = 10.0f; char cValue = 'C'; int *pnValue = &nValue; float *pfValue = &fValue; char *pcValue = &cValue; printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue); printf("pfValue = %x, *pfValue = %f\n", pfValue, *pfValue); printf("pcValue = %x, *pcValue = %c\n", pcValue, *pcValue); return 0; }下面是它对应的反汇编代码(部分):10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: float fValue = 10.0f; 0040126F mov dword ptr [ebp-8],41200000h 12: char cValue = 'C'; 00401276 mov byte ptr [ebp-0Ch],43h 13: int *pnValue = &nValue; 0040127A lea eax,[ebp-4] 0040127D mov dword ptr [ebp-10h],eax 14: float *pfValue = &fValue; 00401280 lea ecx,[ebp-8] 00401283 mov dword ptr [ebp-14h],ecx 15: char *pcValue = &cValue; 00401286 lea edx,[ebp-0Ch] 00401289 mov dword ptr [ebp-18h],edx 16: printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue); 0040128C mov eax,dword ptr [ebp-10h] 0040128F mov ecx,dword ptr [eax] 00401291 push ecx 00401292 mov edx,dword ptr [ebp-10h] 00401295 push edx 00401296 push offset string "pnValue = %x, *pnValue = %d\n" (00432064) 0040129B call printf (00401580) 004012A0 add esp,0Ch从上面的汇编代码可以看到指针变量会占内存空间,它们的地址分别是:[ebp - 10h] 、 [ebp - 14h]、 [ebp - 18h],在给指针变量赋值时首先将变量的地址赋值给临时寄存器,然后将寄存器的值赋值给指针变量,而通过间接访问时也经过了一个临时寄存器,先将指针变量的值赋值给临时寄存器(mov eax,dword ptr [ebp-10h])然后通过这个临时寄存器访问变量的地址空间,得到变量值( mov ecx,dword ptr [eax]),由于间接访问进过了这几步,所以在效率上是比不上直接使用变量。下面是对char型变量的间接访问:004012BF mov edx,dword ptr [ebp-18h] 004012C2 movsx eax,byte ptr [edx] 004012C5 push eax首先也是将指针变量的值取出来,放到寄存器中,然后根据寄存器寻址找到变量对应的地址,访问变量。其中”bye ptr“表示只操作该地址中的一个字节。对于地址我们可以进行加法和减法操作,地址的加法主要用于向下寻址,一般用于数组等占用连续内存空间的数据结构,一般是地址加上一个数值,表示向后偏移一定的单位,指针同样也有这样的操作,但是与地址值不同的是指针每加一个单位,表示向后偏移一个元素,而地址值加1则就是在原来的基础上加上一。指针偏移是根据其所指向的变量类型来决定的,比如有下面的程序:int main(int argc, char* argv[]) { char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89}; int *pInt = (int*)szBuf; short *pShort = (short*)szBuf; char *pChar = szBuf; pInt += 1; pShort += 1; pChar += 1; return 0; }它的汇编代码如下:9: char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89}; 00401028 mov byte ptr [ebp-8],1 0040102C mov byte ptr [ebp-7],23h 00401030 mov byte ptr [ebp-6],45h 00401034 mov byte ptr [ebp-5],67h 00401038 mov byte ptr [ebp-4],89h 10: int *pInt = (int*)szBuf; 0040103C lea eax,[ebp-8] 0040103F mov dword ptr [ebp-0Ch],eax 11: short *pShort = (short*)szBuf; 00401042 lea ecx,[ebp-8] 00401045 mov dword ptr [ebp-10h],ecx 12: char *pChar = szBuf; 00401048 lea edx,[ebp-8] 0040104B mov dword ptr [ebp-14h],edx 13: 14: pInt += 1; 0040104E mov eax,dword ptr [ebp-0Ch] 00401051 add eax,4 00401054 mov dword ptr [ebp-0Ch],eax 15: pShort += 1; 00401057 mov ecx,dword ptr [ebp-10h] 0040105A add ecx,2 0040105D mov dword ptr [ebp-10h],ecx 16: pChar += 1; 00401060 mov edx,dword ptr [ebp-14h] 00401063 add edx,1 00401066 mov dword ptr [ebp-14h],edx根据其汇编代码可以看出,对于int型的指针,每加1个会向后偏移4个字节,short会偏移2个字节,char型的会偏移1个,所以根据以上的内容,可以得出一个公式:TYPE P p + n = p + sizeof(TYPE) n根据上面的加法公式我们可以推导出两个指针的减法公式,TYPE p1, TYPE p2: p2 - p1 = ((int)p2 - (int)p1) / sizeof(TYPE),两个指针相减得到的结果是两个指针之间拥有元素的个数。只有同类型的指针之间才可以相减。而指针的乘除法则没有意义,地址之间的乘除法也没有意义。引用是在C++中提出的,是变量的一个别名,提出引用主要是希望减少指针的使用,引用于指针在一个函数中想上述例子中那样使用并没有太大的意义,大量使用它们是在函数中,作为参数传递,不仅可以节省效率,同时也可以传递一段缓冲,作为输出参数来使用。这大大提升了程序的效率以及灵活性。但是在一些新手程序员看来指针无疑是噩梦般的存在,所以C++引入了引用,希望代替指针。在一般的C++书中都说引用是变量的一个别名是不占内存的,但是我通过查看反汇编代码发现引用并不是向书上说的那样,下面是一段程序及它的反汇编代码:int nValue = 10; int &rValue = nValue; printf("%d\n", rValue);10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: int &rValue = nValue; 0040126F lea eax,[ebp-4] 00401272 mov dword ptr [ebp-8],eax 12: printf("%d\n", rValue); 00401275 mov ecx,dword ptr [ebp-8] 00401278 mov edx,dword ptr [ecx] 0040127A push edx 0040127B push offset string "%d\n" (0042e01c) 00401280 call printf (00401520)从汇编代码中可以看到,在定义引用并为它赋值的过程中,编译器其实是将变量的地址赋值给了一个新的变量,这个变量的地址是[ebp - 8h],在调用printf函数的时候,编译器将地址取出并将它压到函数栈中。下面是将引用改为指针的情况:10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: int *pValue = &nValue; 0040126F lea eax,[ebp-4] 00401272 mov dword ptr [ebp-8],eax 12: printf("%d\n", *pValue); 00401275 mov ecx,dword ptr [ebp-8] 00401278 mov edx,dword ptr [ecx] 0040127A push edx 0040127B push offset string "%d\n" (0042e01c) 00401280 call printf (00401520)两种情况的汇编代码完全一样,也就是说引用其实就是指针,编译器将其包装了一下,使它的行为变得和使用变量相同,而且在语法层面上做了一个限制,引用在定义的时候必须初始化,且初始化完成后就不能指向其他变量,这个行为与常指针相同。
2016年01月03日
3 阅读
0 评论
0 点赞
1
...
28
29
30
31