在编译器编译文件的时候,软件会根据程序本身的要求对函数作不同的压栈处理。有的压栈是按照从左到右进行压栈,有的压栈是按照从右到左进行压栈,有的不压栈、直接用寄存器代替,有的是需要被调用函数自身自己平衡堆栈。下面,我们就可以一个一个自己看看。首先,随便写一个函数,
- int add(int a, int b)
- {
- return a + b;
- }
(1)从右到左压栈
_cdelc是编译器默认的一种压栈方式,在函数上定义不定义其实意义不大。不过,我们愿意贴上代码说明一下,
- int __cdecl add(int a, int b)
- {
- return a + b;
- }
下面,在main函数里面调用此add函数,我们看看对应的汇编是怎么处理的,
- 14: int p = add(2, 3);
- 00401068 push 3
- 0040106A push 2
- 0040106C call @ILT+0(_add) (00401005)
- 00401071 add esp,8
- 00401074 mov dword ptr [ebp-4],eax
- 15: return 1;
- 00401077 mov eax,1
从上面的代码来看,3先压栈,然后是数据2,很明显的从右向左压栈。
(2)从左向右压栈
其实,在windows之前的编译器是支持从左向右进行压栈的,但是现在不支持了。比如说,如果你输入下面这段代码,
- int __pascal add(int a, int b)
- {
- return a + b;
- }
此时,编译器会给你贴上一个错误提示,error C4226: nonstandard extension used : '__pascal' is an obsolete keyword。提示说的很明白,__pascal是一个过时的关键字,现在不支持了。其实堆栈压栈从左向右、还是从右向左其实无所谓。但是如果遇到的函数是变参的话,那么此时就存在问题了。因为对于__pascal而言,最后一个参数不知道究竟是在ebp的哪个偏移位置了?
(3)用寄存器代替压栈
用寄存器代替数据压栈是arm、powerpc等cpu使用的比较多的一种方法。因为用寄存器代替压栈,主要是考虑到速度方面的原因。毕竟取数据、保存数据相比较寄存器操作还是非常耗时间的,其此就是这两种cpu的寄存器资源特别丰富。同样,首先我们要用__fastcall装饰一下函数,
- int __fastcall add(int a, int b)
- {
- return a + b;
- }
那接下来,我们看看调用的时候发生了什么变化,
- 14: int p = add(2, 3);
- 00401068 mov edx,3
- 0040106D mov ecx,2
- 00401072 call @ILT+10(_add) (0040100f)
- 00401077 mov dword ptr [ebp-4],eax
- 15: return 1;
- 0040107A mov eax,1
和上面的压栈不同,这里用edx保存了数据3,用eax保存了数据eax。毕竟寄存器运算要比内存运算快得多。
(4)被调用和自行进行压栈恢复
__stdcall是我们这里讲到的最后一种压栈模式。在函数压栈的方面,他和__cdelc是一样的,但是关键就在add函数结束的位置发生了变化。首先,我们需要用__stdcall装饰了一下函数,
- int __stdcall add(int a, int b)
- {
- return a + b;
- }
那么此时函数汇编的时候,代码发生了变化呢?
- 7: int __stdcall add(int a, int b)
- 8: {
- 00401020 push ebp
- 00401021 mov ebp,esp
- 00401023 sub esp,40h
- 00401026 push ebx
- 00401027 push esi
- 00401028 push edi
- 00401029 lea edi,[ebp-40h]
- 0040102C mov ecx,10h
- 00401031 mov eax,0CCCCCCCCh
- 00401036 rep stos dword ptr [edi]
- 9: return a + b;
- 00401038 mov eax,dword ptr [ebp+8]
- 0040103B add eax,dword ptr [ebp+0Ch]
- 10: }
- 0040103E pop edi
- 0040103F pop esi
- 00401040 pop ebx
- 00401041 mov esp,ebp
- 00401043 pop ebp
- 00401044 ret 8
这里的汇编代码没有什么特别之处。但是最后一个ret 8是什么意思呢?其实因为之前有两个参数a和b,那么8就是这两个参数占有的空间。此时ret b事实上就是让ebp恢复到原来的空间。仅此而已。但是我们发现,可能这一步运行之后,ebp加了不是8,而是12,这又是为什么呢?因为还有4个字节的返回地址没有加上呢。
现代数字通信技术让我们的生活发生了彻底地改变,而通信协议无疑是这一改变的始作俑者。硬件有自己的通信协议,比如pci总线、usb总线、i2c总线等等。软件也有自己的协议栈,无线的3gpp、gprs,有线的tcp/ip协议栈、atm协议等等。基于物理层的协议,很大程度是由芯片完成的,但是涉及到交换路由、数据传输、业务处理,则很大程度上是由软件负责的。协议栈看上去复杂,但是它所使用的技术都是一些基本技术,熟悉这些常用的技术和方法有利于我们在后面的开发中能够高效的利用这些协议。那么,下面我们就要看看,实现协议栈需要怎样的一些基本技术。
(1)状态机
状态机是协议栈使用最多的一种方法。当协议处于不同的状态的时候,就会对不同的报文内容作出不同的处理方法。
(2)定时器
计时器也是协议栈经常使用的方法。通常协议本身在某段时间内需要收到对端发送的响应报文,如果没有收到报文我们就认为通信失败。因此,我们完全可以通过设定定时器的方法,在一段时间之后判断当前的报文有没有发送成功。
(3)重发机制
因为网络的链路状态是十分复杂的,所以不同的协议对报文的响应时间是不同的。只要协议本身允许,一段时间内的重发都是可以的。
(4)校验和
为了验证报文在传输的过程中0和1没有发生改变,在报文中添加校验和也是十分必要的。这种校验方法很多,奇偶校验、crc校验都是可以的。当然,至于具体使用哪种方法需要根据rfc标准来判断。
(5)报文排序
在tcp/ip协议层中,ip层作用就是分片和路由的功能。我们知道有的时候发送的报文是很长的,所以有必要对这些报文进行排序处理。在传输中,我们需要确认所有的报文都能正确地得到传输和处理。
(6)字节序
x86的cpu是高地址高数据,而powerpc是低地址高数据。而报文中的内容需要的是低地址高数据,所以在处理的时候需要十分小心。
(7)其他的基本技术
7.1 互斥 传输涉及到多线程的设计
7.2 链表 链表是模块设计中的基本结构
7.3 最小生成树算法 ospf中需要涉及到基本的图论算法
7.4 rfc标准 rfc是我们一切工作的准绳
7.5 开源软件 开源软件可以帮助我们快速熟悉相关的开发工作
7.6 调试日志 调试日志有助于我们对故障快速进行定位