Young's Blog
包子铺
用汇编对C++代码的优化前后进行分析
下面通过对C++程序汇编代码的分析,加深编译器对C++程序优化的理解
1. C++程序如下:
#include<cstdio>
using std::printf;
class MyParent
{
public:
MyParent()
{}
~MyParent()
{}
void DoJob()
{
Test();
}
private:
virtual void Test()
{
printf("Parent::Test\n");
}
};
class MyChild:public MyParent
{
public:
MyChild()
{}
~MyChild()
{}
private:
virtual void Test()
{
printf("Child::Test\n");
}
};
int main()
{
MyChild obj;
MyParent *p=&obj;
p->DoJob();
}
2.要注意的概念和寄存器表示的约定
2.1 SFP(Stack Frame Pointer)
在Intel的CPU中ESP永远指向栈顶,EBP永远指向栈底,同时栈由高地址向低地址增长。
通常在调用一个函数的时候,下一条指令地址被入栈,方便在函数结束时通过ret返回到下一条
指令继续执行。在查看调用函数的代码时,经常会看到下面的指令
pushl %ebp
mov %esp, %ebp
//建立一个SFP
... ...
leave //相当于mov %ebp, %esp和popl %ebp两条指令
ret
这个SFP用来确定当前函数栈的边界,而且可以通过EBP得到上一层函数栈的栈底。一般通过这种
方式来取传入的参数
mov 0x8(%ebp), xxx
因为建立SFP时pushl的EBP占用4个字节,而之前压入栈的EIP又占用4个字节。
2.2 C++中的vtable和vptr
C++的每个对象都会有一个vptr指针,用来指向vtable的地址,而vtable是一个函数指
针数组,分别指向各个类定义的虚函数。在GCC编译的代码中,vptr在对象的低地址。
*vptr -->得到vtable
(*vptr)[1] -->得到vtable[1]
2.3 颜色约定
这种颜色
表示ESP,
这种颜色
表示EBP,
这种颜色
表示应该要注意的数据。
3.未进行-O2 优化时的汇编代码
代码如下:
Dump of assembler code for function main:
0x080485a0 <main+0>: lea 0x4(%esp),%ecx
0x080485a4 <main+4>: and $0xfffffff0,%esp
0x080485a7 <main+7>: pushl 0xfffffffc(%ecx)
0x080485aa <main+10>: push %ebp
0x080485ab <main+11>: mov %esp,%ebp
0x080485ad <main+13>: push %ebx
0x080485ae <main+14>: push %ecx
0x080485af <main+15>: sub $0x30,%esp
0x080485b2 <main+18>: lea 0xfffffff0(%ebp),%eax
0x080485b5 <main+21>: mov %eax,(%esp)
0x080485b8 <main+24>: call 0x8048650 <_ZN7MyChildC1Ev>
0x080485bd <main+29>: lea 0xfffffff0(%ebp),%eax
0x080485c0 <main+32>: mov %eax,0xfffffff4(%ebp)
0x080485c3 <main+35>: mov 0xfffffff4(%ebp),%eax
0x080485c6 <main+38>: mov %eax,(%esp)
0x080485c9 <main+41>: call 0x8048630 <_ZN8MyParent5DoJobEv>
0x080485ce <main+46>: lea 0xfffffff0(%ebp),%eax
0x080485d1 <main+49>: mov %eax,(%esp)
0x080485d4 <main+52>: call 0x8048670 <_ZN7MyChildD1Ev>
0x080485d9 <main+57>: mov $0x0,%eax
0x080485de <main+62>: mov %eax,0xffffffe4(%ebp)
0x080485e1 <main+65>: jmp 0x8048602 <main+98>
0x080485e3 <main+67>: mov %eax,0xffffffe0(%ebp)
0x080485e6 <main+70>: mov 0xffffffe0(%ebp),%ebx
0x080485e9 <main+73>: lea 0xfffffff0(%ebp),%eax
0x080485ec <main+76>: mov %eax,(%esp)
0x080485ef <main+79>: call 0x8048670 <_ZN7MyChildD1Ev>
0x080485f4 <main+84>: mov %ebx,0xffffffe0(%ebp)
0x080485f7 <main+87>: mov 0xffffffe0(%ebp),%eax
0x080485fa <main+90>: mov %eax,(%esp)
0x080485fd <main+93>: call 0x8048480 <_init+100>
0x08048602 <main+98>: mov 0xffffffe4(%ebp),%eax
0x08048605 <main+101>: add $0x30,%esp
0x08048608 <main+104>: pop %ecx
0x08048609 <main+105>: pop %ebx
0x0804860a <main+106>: pop %ebp
0x0804860b <main+107>: lea 0xfffffffc(%ecx),%esp
0x0804860e <main+110>: ret
0x0804860f <main+111>: nop
End of assembler dump.
从这里开始分析整个代码:
Dump of assembler code for function main:
0x080485a0 <main+0>: lea 0x4(%esp),%ecx
0x080485a4 <main+4>: and $0xfffffff0,%esp
0x080485a7 <main+7>: pushl 0xfffffffc(%ecx)
0x080485aa <main+10>: push %ebp
0x080485ab <main+11>: mov %esp,%ebp
--------------------------------------------------------------------------
上面这段代码做了进入main函数时的准备工作,同时创建了SPF,此时的堆栈与寄存器如下:
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0xbfbfe868 0x2824a6b9
0xbfbfe850: 0x00000018 0x00000001
0xbfbfe88c
0x08048529
6: /x $eax = 0xbfbfe894
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe858
========================== 下面继续代码======================================
0x080485ad <main+13>: push %ebx
0x080485ae <main+14>: push %ecx
0x080485af <main+15>: sub $0x30,%esp
0x080485b2 <main+18>: lea 0xfffffff0(%ebp),%eax
0x080485b5 <main+21>: mov %eax,(%esp)
0x080485b8 <main+24>: call 0x8048650 <_ZN7MyChildC1Ev>
--------------------------------------------------------------------------
这两段代码分别完成:
1.首先保存EBX和ECX的数据,然后准备了48(0x30)个字节的空间,同时保存MyChild的this指
针的地址到EAX。
2.把this指针入栈,调用MyChild::MyChild()进行构造。
调用MyChild::MyChild()之前的堆栈与寄存器内容如下:
0xbfbfe800: 0x28070814 0xbfbfe844 0x2804d998 0x28074e24
0xbfbfe810: 0x00000001 0xbfbfe834 0x00000001 0x00000000
0xbfbfe820
:
0xbfbfe848
[1]
0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000
0xbfbfe868
[2]
0x2824a6b9
[3]
0xbfbfe850: 0xbfbfe870 0x00000001
0xbfbfe88c
0x08048529
6: /x $eax =
0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe820
这里[1]保存着this指针(也就是[2]的地址),注意[2]后面的[3]
,就是MyParent的指针空间
(MyParent *p),现在保存的地址为0x2824a6b9还没有进行赋值。
在C++中一个空类的sizeof大小为1,这个值是在编译时期就计算出来的,而在实际情况中,如果
一个空类没有虚函数,那么在内存中占1个字节,否则就占用4个字节(vptr用)。
此时[2]这块空间就是vptr指针的空间,[2]的地址则是this指针所指向的地址,这个时候vptr还
没有初始化。
下面要开始调用MyChild::MyChild(),代码如下:
Dump of assembler code for function _ZN7MyChildC1Ev:
0x08048650 <_ZN7MyChildC1Ev+0>: push %ebp
0x08048651 <_ZN7MyChildC1Ev+1>: mov %esp,%ebp
0x08048653 <_ZN7MyChildC1Ev+3>: sub $0x8,%esp
0x08048656 <_ZN7MyChildC1Ev+6>: mov 0x8(%ebp),%eax
0x08048659 <_ZN7MyChildC1Ev+9>: mov %eax,(%esp)
0x0804865c <_ZN7MyChildC1Ev+12>: call 0x8048610 <_ZN8MyParentC2Ev>
0x08048661 <_ZN7MyChildC1Ev+17>: mov $0x8048780,%edx
0x08048666 <_ZN7MyChildC1Ev+22>: mov 0x8(%ebp),%eax
0x08048669 <_ZN7MyChildC1Ev+25>: mov %edx,(%eax)
0x0804866b <_ZN7MyChildC1Ev+27>: leave
0x0804866c <_ZN7MyChildC1Ev+28>: ret
0x0804866d <_ZN7MyChildC1Ev+29>: nop
0x0804866e <_ZN7MyChildC1Ev+30>: nop
0x0804866f <_ZN7MyChildC1Ev+31>: nop
End of assembler dump.
我们从这里开始分析MyChild::MyChild()的代码:
Dump of assembler code for function _ZN7MyChildC1Ev:
0x08048650 <_ZN7MyChildC1Ev+0>: push %ebp
0x08048651 <_ZN7MyChildC1Ev+1>: mov %esp,%ebp
0x08048653 <_ZN7MyChildC1Ev+3>: sub $0x8,%esp
0x08048656 <_ZN7MyChildC1Ev+6>: mov 0x8(%ebp),%eax
--------------------------------------------------------------------------
这里建立SFP后,最后把刚才传入的this指针保存到EAX中。从堆栈的内容可以知道,EBP+8后刚好
是
[2],地址为 0xbfbfe820,这里保存就是入参;[1]的位置保存着EIP。
0xbfbfe800: 0x28070814 0xbfbfe844 0x2804d998 0x28074e24
0xbfbfe810
:
0x00000001
0xbfbfe834
0xbfbfe858
0x080485bd
[1]
0xbfbfe820:
0xbfbfe848
[2]
0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0xbfbfe868 0x2824a6b9
0xbfbfe850: 0xbfbfe870 0x00000001 0xbfbfe88c 0x08048529
6: /x $eax = 0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe818
1: /x $esp =
0xbfbfe810
========================== 下面继续代码======================================
0x08048659 <_ZN7MyChildC1Ev+9>: mov %eax,(%esp)
0x0804865c <_ZN7MyChildC1Ev+12>: call 0x8048610 <_ZN8MyParentC2Ev>
--------------------------------------------------------------------------
再次把this指针入栈,然后调用MyChild的基类MyParent::MyParent构造函数,代码如下:
Dump of assembler code for function _ZN8MyParentC2Ev:
0x08048610 <_ZN8MyParentC2Ev+0>: push %ebp
0x08048611 <_ZN8MyParentC2Ev+1>: mov %esp,%ebp
0x08048613 <_ZN8MyParentC2Ev+3>: mov $0x80487b8,%edx
0x08048618 <_ZN8MyParentC2Ev+8>: mov 0x8(%ebp),%eax
0x0804861b <_ZN8MyParentC2Ev+11>: mov %edx,(%eax)
0x0804861d <_ZN8MyParentC2Ev+13>: pop %ebp
0x0804861e <_ZN8MyParentC2Ev+14>: ret
0x0804861f <_ZN8MyParentC2Ev+15>: nop
End of assembler dump.
我们从这里开始分析MyParent::MyParent的代码:
0x08048610 <_ZN8MyParentC2Ev+0>: push %ebp
0x08048611 <_ZN8MyParentC2Ev+1>: mov %esp,%ebp
0x08048613 <_ZN8MyParentC2Ev+3>: mov $0x80487b8,%edx
--------------------------------------------------------------------------
同样地建立SFP.但是
0x80487b8是什么?这个是MyParent的vtable地址,而EDX我们就是所谓
的vptr指针了。我们可以验证一下:
(gdb) x 0x80487b8
0x80487b8 <_ZTV8MyParent+8>: 0x080486b0
由于vtable是一个函数指针数组,所以我们用"x
0x80487b8"得到的
0x080486b0其实是vtable[0],
接下来:
(gdb) x 0x080486b0
0x80486b0 <_ZN8MyParent4TestEv>: 0x83e58955
这个才是MyParent::Test的真实地址。用C++filt还原上面的"
_ZTV8MyParent+8
"和
"
_ZN8MyParent4TestEv":
_ZTV8MyParent+8 vtable for MyParent+8
_ZN8MyParent4TestEv MyParent::Test()
==========================下面 继续代码======================================
0x08048618 <_ZN8MyParentC2Ev+8>: mov 0x8(%ebp),%eax
0x0804861b <_ZN8MyParentC2Ev+11>: mov %edx,(%eax)
--------------------------------------------------------------------------
得到MyParent的vtable地址后,我们需要设置MyChild的vptr指向MyParent的vtable。这里
首先取出来的就是MyChild对象的this指针,前面也说过了,vptr指针在对象的头4个字节,所以这时候
EAX保存的也就是MyChild的vptr的地址。
然后把MyParent的vtable地址赋值给MyChild的vptr,堆栈和寄存器内容如下:
0xbfbfe800: 0x28070814 0xbfbfe844
0xbfbfe818
0x08048661
0xbfbfe810:
0xbfbfe848
[1]
0xbfbfe834 0xbfbfe858 0x080485bd
0xbfbfe820: 0xbfbfe848 0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000
0x080487b8
[2]
0x2824a6b9
0xbfbfe850: 0xbfbfe870 0x00000001 0xbfbfe88c 0x08048529
6: /x $eax =
0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx =
0x80487b8
2: /x $ebp =
0xbfbfe808
1: /x $esp =
0xbfbfe808
由于这里ESP和EBP是一样的地址,[1]实际上是0x8(%ebp)的位置,[2]是MyChild对象的vptr指针
的空间,现在已经指向MyParent的vtable地址了
==========================下面 继续代码======================================
0x0804861d <_ZN8MyParentC2Ev+13>: pop %ebp
0x0804861e <_ZN8MyParentC2Ev+14>: ret
0x0804861f <_ZN8MyParentC2Ev+15>: nop
--------------------------------------------------------------------------
由于没有增加堆栈空间,EBP和ESP是一样的,所以不需要LEAVE指令,直接POP出EBP。
从这里结束对MyParent::MyParent()的分析,下面继续MyChild::MyChild()的分析
==========================下面 继续代码======================================
0x08048661 <_ZN7MyChildC1Ev+17>: mov $0x8048780,%edx
0x08048666 <_ZN7MyChildC1Ev+22>: mov 0x8(%ebp),%eax
0x08048669 <_ZN7MyChildC1Ev+25>: mov %edx,(%eax)
0x0804866b <_ZN7MyChildC1Ev+27>: leave
0x0804866c <_ZN7MyChildC1Ev+28>: ret
0x0804866d <_ZN7MyChildC1Ev+29>: nop
0x0804866e <_ZN7MyChildC1Ev+30>: nop
0x0804866f <_ZN7MyChildC1Ev+31>: nop
--------------------------------------------------------------------------
从MyParent::MyParent()返回以后,现在执行MyChild::MyChild()。这里首先也是把vtable
地址保存到EDX,取出MyChild对象的this指针,然后设置vptr指向MyChild的vtable.由于整个构造函数
都是空的,所以这里设置完vptr后就直接返回,下面是设置完vptr后的寄存器和堆栈内容:
0xbfbfe800: 0x28070814 0xbfbfe844 0xbfbfe818 0x08048661
0xbfbfe810:
0xbfbfe848
0xbfbfe834
0xbfbfe858
0x080485bd
0xbfbfe820:
0xbfbfe848
[1]
0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000
0x08048780
[2]
0x2824a6b9
0xbfbfe850: 0xbfbfe870 0x00000001 0xbfbfe88c 0x08048529
6: /x $eax =
0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx =
0x8048780
2: /x $ebp =
0xbfbfe818
1: /x $esp =
0xbfbfe810
这里[1]是0x8(%ebp)的位置,[2]是(%eax)的位置。可以看到已经把EDX的值赋给[2]了。
这里结束对MyChild::MyChild()代码的分析。
==========================下面继 续代码======================================
0x080485bd <main+29>: lea 0xfffffff0(%ebp),%eax
0x080485c0 <main+32>: mov %eax,0xfffffff4(%ebp)
0x080485c3 <main+35>: mov 0xfffffff4(%ebp),%eax
0x080485c6 <main+38>: mov %eax,(%esp)
0x080485c9 <main+41>: call 0x8048630 <_ZN8MyParent5DoJobEv>
--------------------------------------------------------------------------
构造函数执行完毕后,我们把MyChild对象的指针给了MyParent指针,然后调用了DoJob方法。
在调用DoJob之前的寄存器和堆栈地址为:
0xbfbfe800: 0x28070814 0xbfbfe844 0xbfbfe818 0x08048661
0xbfbfe810: 0xbfbfe848 0xbfbfe834 0xbfbfe858 0x080485bd
0xbfbfe820:
0xbfbfe848
[3]
0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000
0x08048780
[1]
0xbfbfe848
[2]
0xbfbfe850: 0xbfbfe870 0x00000001
0xbfbfe88c
0x08048529
6: /x $eax =
0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x8048780
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe820
首先保存[1]的地址
(this指针)
到EAX,再保存到[2],这里是"MyParent *p"的指针空间。这样子就完
成了语句"MyParent *p=&obj"的赋值,然后再把"MyParent *p"所保存的地址入栈为调用MyParent::
DoJob做准备。
MyParent::DoJob的代码如下:
Dump of assembler code for function _ZN8MyParent5DoJobEv:
0x08048630 <_ZN8MyParent5DoJobEv+0>: push %ebp
0x08048631 <_ZN8MyParent5DoJobEv+1>: mov %esp,%ebp
0x08048633 <_ZN8MyParent5DoJobEv+3>: sub $0x8,%esp
0x08048636 <_ZN8MyParent5DoJobEv+6>: mov 0x8(%ebp),%eax
0x08048639 <_ZN8MyParent5DoJobEv+9>: mov (%eax),%eax
0x0804863b <_ZN8MyParent5DoJobEv+11>: mov (%eax),%edx
0x0804863d <_ZN8MyParent5DoJobEv+13>: mov 0x8(%ebp),%eax
0x08048640 <_ZN8MyParent5DoJobEv+16>: mov %eax,(%esp)
0x08048643 <_ZN8MyParent5DoJobEv+19>: call *%edx
0x08048645 <_ZN8MyParent5DoJobEv+21>: leave
0x08048646 <_ZN8MyParent5DoJobEv+22>: ret
0x08048647 <_ZN8MyParent5DoJobEv+23>: nop
0x08048648 <_ZN8MyParent5DoJobEv+24>: nop
0x08048649 <_ZN8MyParent5DoJobEv+25>: nop
0x0804864a <_ZN8MyParent5DoJobEv+26>: nop
0x0804864b <_ZN8MyParent5DoJobEv+27>: nop
0x0804864c <_ZN8MyParent5DoJobEv+28>: nop
0x0804864d <_ZN8MyParent5DoJobEv+29>: nop
0x0804864e <_ZN8MyParent5DoJobEv+30>: nop
0x0804864f <_ZN8MyParent5DoJobEv+31>: nop
End of assembler dump.
从这里开始MyParent::DoJob的代码分析:
0x08048630 <_ZN8MyParent5DoJobEv+0>: push %ebp
0x08048631 <_ZN8MyParent5DoJobEv+1>: mov %esp,%ebp
0x08048633 <_ZN8MyParent5DoJobEv+3>: sub $0x8,%esp
0x08048636 <_ZN8MyParent5DoJobEv+6>: mov 0x8(%ebp),%eax
0x08048639 <_ZN8MyParent5DoJobEv+9>: mov (%eax),%eax
0x0804863b <_ZN8MyParent5DoJobEv+11>: mov (%eax),%edx
0x0804863d <_ZN8MyParent5DoJobEv+13>: mov 0x8(%ebp),%eax
0x08048640 <_ZN8MyParent5DoJobEv+16>: mov %eax,(%esp)
0x08048643 <_ZN8MyParent5DoJobEv+19>: call *%edx
--------------------------------------------------------------------------
上面的代码分成三部分:
1.
这个时候已经把堆栈中的 this指针取出来保存到EAX中。
2.取出MyChild的vtable地址保存到EAX
,再取出vtable[0]保存到EDX
3.取出堆栈中的this指针保存到EAX,同时入栈,然后调用vtable[0]的函数
在调用vtable[0]函数之前的寄存器和堆栈数据如下:
0xbfbfe800: 0x28070814 0xbfbfe844 0xbfbfe818 0x08048661
0xbfbfe810:
0xbfbfe848
0xbfbfe834
0xbfbfe858
0x080485ce
0xbfbfe820:
0xbfbfe848
[1]
0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000
0x08048780
[2]
0xbfbfe848
0xbfbfe850: 0xbfbfe870 0x00000001 0xbfbfe88c 0x08048529
6: /x $eax =
0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx =
0x8048690
2: /x $ebp =
0xbfbfe818
1: /x $esp =
0xbfbfe810
先从[1]的地方取出this指针保存到EAX中,然后再通过
"mov (%eax),%eax",取出[2](0x08048780)
保存到EAX,最后再通过"
mov (%eax),%edx"取出vtable保存到EDX中,我们验证一下:
(gdb) x 0x08048780
0x8048780 <_ZTV7MyChild+8>: 0x08048690
此时EDX保存的就是
0x08048690,最后 我们调用"
call *%edx"的时候,就等于调用vtable[0]:
(gdb) x 0x08048690
0x8048690 <_ZN7MyChild4TestEv>: 0x83e58955
用c++filt对上面的字符串进行还原:
_ZTV7MyChild+8
vtable for MyChild+8
_ZN7MyChild4TestEv MyChild::Test()
现在下一步就直接进入MyChild::Test执行,代码如下:
Dump of assembler code for function _ZN7MyChild4TestEv:
0x08048690 <_ZN7MyChild4TestEv+0>: push %ebp
0x08048691 <_ZN7MyChild4TestEv+1>: mov %esp,%ebp
0x08048693 <_ZN7MyChild4TestEv+3>: sub $0x8,%esp
0x08048696 <_ZN7MyChild4TestEv+6>: movl $0x804875d,(%esp)
0x0804869d <_ZN7MyChild4TestEv+13>: call 0x8048440 <_init+36>
0x080486a2 <_ZN7MyChild4TestEv+18>: leave
0x080486a3 <_ZN7MyChild4TestEv+19>: ret
0x080486a4 <_ZN7MyChild4TestEv+20>: nop
0x080486a5 <_ZN7MyChild4TestEv+21>: nop
0x080486a6 <_ZN7MyChild4TestEv+22>: nop
0x080486a7 <_ZN7MyChild4TestEv+23>: nop
0x080486a8 <_ZN7MyChild4TestEv+24>: nop
0x080486a9 <_ZN7MyChild4TestEv+25>: nop
0x080486aa <_ZN7MyChild4TestEv+26>: nop
0x080486ab <_ZN7MyChild4TestEv+27>: nop
0x080486ac <_ZN7MyChild4TestEv+28>: nop
0x080486ad <_ZN7MyChild4TestEv+29>: nop
0x080486ae <_ZN7MyChild4TestEv+30>: nop
0x080486af <_ZN7MyChild4TestEv+31>: nop
End of assembler dump.
从这里我们分析MyChild::Test()的代码:
0x08048690 <_ZN7MyChild4TestEv+0>: push %ebp
0x08048691 <_ZN7MyChild4TestEv+1>: mov %esp,%ebp
0x08048693 <_ZN7MyChild4TestEv+3>: sub $0x8,%esp
0x08048696 <_ZN7MyChild4TestEv+6>: movl $0x804875d,(%esp)
0x0804869d <_ZN7MyChild4TestEv+13>: call 0x8048440 <_init+36>
0x080486a2 <_ZN7MyChild4TestEv+18>: leave
0x080486a3 <_ZN7MyChild4TestEv+19>: ret
--------------------------------------------------------------------------
由于MyChild::Test()只是简单地打印字符串,所以这里并没有用到堆栈中的this指针,只是把字符
串常量的地址
0x804875d
入栈,然后打印。在调用call之前的寄存器和堆栈如下:
0xbfbfe7f0: 0x28070814 0xbfbfe804 0x2804fd26 0x28078040
0xbfbfe800:
0x0804875d
0xbfbfe844
0xbfbfe818
0x08048645
0xbfbfe810: 0xbfbfe848 0xbfbfe834 0xbfbfe858 0x080485ce
0xbfbfe820: 0xbfbfe848 0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0x08048780 0xbfbfe848
0xbfbfe850: 0xbfbfe870 0x00000001 0xbfbfe88c 0x08048529
6: /x $eax = 0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x8048690
2: /x $ebp =
0xbfbfe808
1: /x $esp =
0xbfbfe800
我们来看一下
0x804875d的内容:
(gdb) x /s 0x0804875d
0x804875d <_fini+97>: "Child::Test"
而
这里调用的0x8048440是标准库中的puts函 数。到这里为止,接下来的MyChild::Test的代码就是
返回和nop指令了,我们直接从MyChild::Test返回到MyParent::DoJob的执行中
从这里结束MyChild::Test()的代码分析
--------------------------------------------------------------------------
由于MyParent::DoJob()剩下的也是返回和nop指令,这里直接返回到main中
从这里结束MyParent::DoJob的代码分析
--------------------------------------------------------------------------
从MyParent::DoJob返回后,寄存器和堆栈的内容如下:
0xbfbfe800: 0x0804875d 0xbfbfe844 0xbfbfe818 0x08048645
0xbfbfe810: 0xbfbfe848 0xbfbfe834 0xbfbfe858 0x080485ce
0xbfbfe820:
0xbfbfe848
0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0x08048780 0xbfbfe848
0xbfbfe850: 0xbfbfe870 0x00000001
0xbfbfe88c
0x08048529
6: /x $eax = 0xa
5: /x $ebx = 0x1
4: /x $ecx = 0xc
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe820
========================== 下面继续代码======================================
0x080485ce <main+46>: lea 0xfffffff0(%ebp),%eax
0x080485d1 <main+49>: mov %eax,(%esp)
0x080485d4 <main+52>: call 0x8048670 <_ZN7MyChildD1Ev>
--------------------------------------------------------------------------
执行完DoJob后,现在整个main函数要结束,接下来首先调用的就是MyChild::~MyChild()。
这里传入this指针到EAX,然后入栈调用MyChild的析构函数。在调用析构函数之前的寄存器和堆栈内容如下:
0xbfbfe800: 0x0804875d 0xbfbfe844 0xbfbfe818 0x08048645
0xbfbfe810: 0xbfbfe848 0xbfbfe834 0xbfbfe858 0x080485ce
0xbfbfe820:
0xbfbfe848
[1]
0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0x08048780 0xbfbfe848
0xbfbfe850: 0xbfbfe870 0x00000001
0xbfbfe88c
0x08048529
6: /x $eax = 0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xc
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe820
上面的[1]为入栈的this指针,MyChild::~MyChild()的代码如下:
Dump of assembler code for function _ZN7MyChildD1Ev:
0x08048670 <_ZN7MyChildD1Ev+0>: push %ebp
0x08048671 <_ZN7MyChildD1Ev+1>: mov %esp,%ebp
0x08048673 <_ZN7MyChildD1Ev+3>: sub $0x8,%esp
0x08048676 <_ZN7MyChildD1Ev+6>: mov $0x8048780,%eax
0x0804867b <_ZN7MyChildD1Ev+11>: mov 0x8(%ebp),%edx
0x0804867e <_ZN7MyChildD1Ev+14>: mov %eax,(%edx)
0x08048680 <_ZN7MyChildD1Ev+16>: mov 0x8(%ebp),%eax
0x08048683 <_ZN7MyChildD1Ev+19>: mov %eax,(%esp)
0x08048686 <_ZN7MyChildD1Ev+22>: call 0x8048620 <_ZN8MyParentD2Ev>
0x0804868b <_ZN7MyChildD1Ev+27>: leave
0x0804868c <_ZN7MyChildD1Ev+28>: ret
0x0804868d <_ZN7MyChildD1Ev+29>: nop
0x0804868e <_ZN7MyChildD1Ev+30>: nop
0x0804868f <_ZN7MyChildD1Ev+31>: nop
End of assembler dump.
从这里开始分析MyChild::~MyChild()的代码:
0x08048670 <_ZN7MyChildD1Ev+0>: push %ebp
0x08048671 <_ZN7MyChildD1Ev+1>: mov %esp,%ebp
0x08048673 <_ZN7MyChildD1Ev+3>: sub $0x8,%esp
0x08048676 <_ZN7MyChildD1Ev+6>: mov $0x8048780,%eax
0x0804867b <_ZN7MyChildD1Ev+11>: mov 0x8(%ebp),%edx
0x0804867e <_ZN7MyChildD1Ev+14>: mov %eax,(%edx)
0x08048680 <_ZN7MyChildD1Ev+16>: mov 0x8(%ebp),%eax
0x08048683 <_ZN7MyChildD1Ev+19>: mov %eax,(%esp)
0x08048686 <_ZN7MyChildD1Ev+22>: call 0x8048620 <_ZN8MyParentD2Ev>
--------------------------------------------------------------------------
上面的代码分成三部分
1.建立SFP,保留8字节堆栈空间
2.设置MyChild的vtable到EAX,保存this指针到EDX,然后设置this指针的vptr指向MyChild的vtable
3.this指针入栈,调用
MyParent::~MyParent()
函数
在调用
MyParent::~MyParent()之前的寄存器和堆栈内容如下:
0xbfbfe800: 0x0804875d 0xbfbfe844 0xbfbfe818 0x08048645
0xbfbfe810:
0xbfbfe848
0xbfbfe834
0xbfbfe858
0x080485d9
0xbfbfe820: 0xbfbfe848 0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830: 0xbfbfe9a0 0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000
0x08048780
[1]
0xbfbfe848
0xbfbfe850: 0xbfbfe870 0x00000001 0xbfbfe88c 0x08048529
6: /x $eax = 0xbfbfe848
5: /x $ebx = 0x1
4: /x $ecx = 0xc
3: /x $edx = 0xbfbfe848
2: /x $ebp =
0xbfbfe818
1: /x $esp =
0xbfbfe810
上面[1]为MyChild的vtable地址,验证如下:
(gdb) x 0x08048780
0x8048780 <_ZTV7MyChild+8>: 0x08048690
(gdb) x 0x08048690
0x8048690 <_ZN7MyChild4TestEv>: 0x83e58955
经过c++filt的还原:
_ZTV7MyChild+8 vtable for MyChild+8
_ZN7MyChild4TestEv MyChild::Test()
MyParent::~MyParent()的代码如下:
Dump of assembler code for function _ZN8MyParentD2Ev:
0x08048620 <_ZN8MyParentD2Ev+0>: push %ebp
0x08048621 <_ZN8MyParentD2Ev+1>: mov %esp,%ebp
0x08048623 <_ZN8MyParentD2Ev+3>: mov $0x80487b8,%edx
0x08048628 <_ZN8MyParentD2Ev+8>: mov 0x8(%ebp),%eax
0x0804862b <_ZN8MyParentD2Ev+11>: mov %edx,(%eax)
0x0804862d <_ZN8MyParentD2Ev+13>: pop %ebp
0x0804862e <_ZN8MyParentD2Ev+14>: ret
0x0804862f <_ZN8MyParentD2Ev+15>: nop
End of assembler dump.
除了把vtable地址保存到EDX之后,再赋值给this指针的vptr,其他的什么也没有做。
下面接着从MyChild::~MyChild()返回之后
==========================下面继 续代码======================================
0x080485d9 <main+57>: mov $0x0,%eax
0x080485de <main+62>: mov %eax,0xffffffe4(%ebp)
0x080485e1 <main+65>: jmp 0x8048602 <main+98>
0x080485e3 <main+67>: mov %eax,0xffffffe0(%ebp)
0x080485e6 <main+70>: mov 0xffffffe0(%ebp),%ebx
0x080485e9 <main+73>: lea 0xfffffff0(%ebp),%eax
0x080485ec <main+76>: mov %eax,(%esp)
0x080485ef <main+79>: call 0x8048670 <_ZN7MyChildD1Ev>
0x080485f4 <main+84>: mov %ebx,0xffffffe0(%ebp)
0x080485f7 <main+87>: mov 0xffffffe0(%ebp),%eax
0x080485fa <main+90>: mov %eax,(%esp)
0x080485fd <main+93>: call 0x8048480 <_init+100>
0x08048602 <main+98>: mov 0xffffffe4(%ebp),%eax
0x08048605 <main+101>: add $0x30,%esp
0x08048608 <main+104>: pop %ecx
0x08048609 <main+105>: pop %ebx
0x0804860a <main+106>: pop %ebp
0x0804860b <main+107>: lea 0xfffffffc(%ecx),%esp
0x0804860e <main+110>: ret
0x0804860f <main+111>: nop
End of assembler dump.
--------------------------------------------------------------------------
剩下的三部分代码主要就是做出栈,异常处理和后继的工作
1.上面执行到"
jmp 0x8048602 <main+98>"后就跳到第3部分
2.这块调用_Unwind_Resume,属于C++的异常处理部分
3.出栈处理,从main函数返回
4.进行-O2优化后的代码
代码如下:
Dump of assembler code for function main:
0x080485a0 <main+0>: lea 0x4(%esp),%ecx
0x080485a4 <main+4>: and $0xfffffff0,%esp
0x080485a7 <main+7>: pushl 0xfffffffc(%ecx)
0x080485aa <main+10>: push %ebp
0x080485ab <main+11>: mov %esp,%ebp
0x080485ad <main+13>: push %ecx
0x080485ae <main+14>: sub $0x24,%esp
0x080485b1 <main+17>: lea 0xfffffff8(%ebp),%eax
0x080485b4 <main+20>: movl $0x80486b0,0xfffffff8(%ebp)
0x080485bb <main+27>: mov %eax,(%esp)
0x080485be <main+30>: call *0x80486b0
0x080485c4 <main+36>: add $0x24,%esp
0x080485c7 <main+39>: xor %eax,%eax
0x080485c9 <main+41>: pop %ecx
0x080485ca <main+42>: pop %ebp
0x080485cb <main+43>: lea 0xfffffffc(%ecx),%esp
0x080485ce <main+46>: ret
0x080485cf <main+47>: mov %eax,(%esp)
0x080485d2 <main+50>: call 0x8048480 <_init+100>
0x080485d7 <main+55>: nop
0x080485d8 <main+56>: nop
0x080485d9 <main+57>: nop
0x080485da <main+58>: nop
0x080485db <main+59>: nop
0x080485dc <main+60>: nop
0x080485dd <main+61>: nop
0x080485de <main+62>: nop
0x080485df <main+63>: nop
End of assembler dump.
下面开始分析
代码
:
0x080485a0 <main+0>: lea 0x4(%esp),%ecx
0x080485a4 <main+4>: and $0xfffffff0,%esp
0x080485a7 <main+7>: pushl 0xfffffffc(%ecx)
0x080485aa <main+10>: push %ebp
0x080485ab <main+11>: mov %esp,%ebp
0x080485ad <main+13>: push %ecx
0x080485ae <main+14>: sub $0x24,%esp
--------------------------------------------------------------------------
程序开始,建立SFP,ECX入栈并保存36个字节空间后的寄存器和堆栈内容如下:
0xbfbfe810: 0x00000001 0xbfbfe834 0x00000001 0x00000000
0xbfbfe820: 0x00000000 0x00000001 0xbfbfe984 0xbfbfe98c
0xbfbfe830:
0xbfbfe9a4
0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe8a0 0x00000000 0xbfbfe868 0x2824a6b9
0xbfbfe850: 0x00000020 0xbfbfe870
0xbfbfe890
0x08048569
7: /x $eax = 0xbfbfe898
6: /x $ebx = 0x1
5: /x $ecx = 0xbfbfe870
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe830
==========================下面继续代码======================================
0x080485b1 <main+17>: lea 0xfffffff8(%ebp),%eax
0x080485b4 <main+20>: movl $0x80486b0,0xfffffff8(%ebp)
--------------------------------------------------------------------------
这段代码把this指针保存到EAX中,再把MyChild的vtable直接赋值给[1](这里就是this指针)
的vptr空间。
寄存器和堆栈内容如下:
0xbfbfe810: 0x00000001 0xbfbfe834 0x00000001 0x00000000
0xbfbfe820: 0x00000000 0x00000001 0xbfbfe980 0xbfbfe988
0xbfbfe830:
0xbfbfe9a0
0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0xbfbfe868 0x2824a6b9
0xbfbfe850:
0x080486b0
[1]
0xbfbfe870
0xbfbfe88c
0x08048529
6: /x $eax =
0xbfbfe850
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe830
==========================下面继续代码======================================
0x080485bb <main+27>: mov %eax,(%esp)
0x080485be <main+30>: call *0x80486b0
--------------------------------------------------------------------------
这里this指针入栈后,并不通过vptr,直接调用vtable[0]的函数,此时vptr形同虚设,直接被
编译器无视。验证一下
*0x80486b0 的处理函数:
(gdb) x 0x80486b0
0x80486b0 <_ZTV7MyChild+8>: 0x080485e0
(gdb) x 0x080485e0
0x80485e0 <_ZN7MyChild4TestEv>: 0xc7e58955
进行还原后
_ZTV7MyChild+8
vtable for MyChild+8
_ZN7MyChild4TestEv MyChild::Test()
MyChild::Test()的代码如下:
Dump of assembler code for function _ZN7MyChild4TestEv:
0x080485e0 <_ZN7MyChild4TestEv+0>: push %ebp
0x080485e1 <_ZN7MyChild4TestEv+1>: mov %esp,%ebp
0x080485e3 <_ZN7MyChild4TestEv+3>: movl $0x804868c,0x8(%ebp)
0x080485ea <_ZN7MyChild4TestEv+10>: pop %ebp
0x080485eb <_ZN7MyChild4TestEv+11>: jmp 0x8048440 <_init+36>
End of assembler dump.
从这里开始MyChild::Test()的代码分析:
0x080485e0 <_ZN7MyChild4TestEv+0>: push %ebp
0x080485e1 <_ZN7MyChild4TestEv+1>: mov %esp,%ebp
0x080485e3 <_ZN7MyChild4TestEv+3>: movl $0x804868c,0x8(%ebp)
--------------------------------------------------------------------------
这里的"
movl $0x804868c,0x8(%ebp)"直接破坏掉刚才入栈传入的this指针,
0x804868c
地址保存的就是我们要打印的字符串:
(gdb) x /s 0x0804868c
0x804868c <_fini+96>: "Child::Test"
==========================下面 继续代码======================================
0x080485ea <_ZN7MyChild4TestEv+10>: pop %ebp
0x080485eb <_ZN7MyChild4TestEv+11>: jmp 0x8048440 <_init+36>
--------------------------------------------------------------------------
保存字符串地址在堆栈中后,现在直接构造puts函数调用之前的堆栈,由于每次在进入函数时都会执行
"push %ebp"和"mov %esp, %ebp"而且每次读取入参都是通过0x8(%ebp)来读取,所以这里必需要先
pop掉EBP,一但jmp进入puts函数,执行"push %ebp"和"mov %esp, %ebp"后,字符串地址存放的位
置刚好就是0x8(%ebp)。
pop %ebp之前的堆栈([1]为字符串地址):
0xbfbfe810: 0x00000001 0xbfbfe834 0x00000001 0x00000000
0xbfbfe820: 0x00000000 0x00000001
0xbfbfe858
0x080485c4
0xbfbfe830:
0x0804868c
[1]
0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0xbfbfe868 0x2824a6b9
0xbfbfe850: 0x080486b0 0xbfbfe870 0xbfbfe88c 0x08048529
6: /x $eax = 0xbfbfe850
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe828
1: /x $esp =
0xbfbfe828
pop %ebp之后的堆栈,在jmp进入之前:
0xbfbfe810: 0x00000001 0xbfbfe834 0x00000001 0x00000000
0xbfbfe820: 0x00000000 0x00000001 0xbfbfe858
0x080485c4
0xbfbfe830:
0x0804868c
[1]
0x2828cdc0 0xbfbfe858 0x00000001
0xbfbfe840: 0xbfbfe89c 0x00000000 0xbfbfe868 0x2824a6b9
0xbfbfe850: 0x080486b0 0xbfbfe870
0xbfbfe88c
0x08048529
6: /x $eax = 0xbfbfe850
5: /x $ebx = 0x1
4: /x $ecx = 0xbfbfe870
3: /x $edx = 0x0
2: /x $ebp =
0xbfbfe858
1: /x $esp =
0xbfbfe82c
在jmp进入puts函数后,打印出结果后程序就结束了。main函数中的下面的这部分代码没有执行过
========================== 下面继续代码======================================
0x080485c4 <main+36>: add $0x24,%esp
0x080485c7 <main+39>: xor %eax,%eax
0x080485c9 <main+41>: pop %ecx
0x080485ca <main+42>: pop %ebp
0x080485cb <main+43>: lea 0xfffffffc(%ecx),%esp
0x080485ce <main+46>: ret
0x080485cf <main+47>: mov %eax,(%esp)
0x080485d2 <main+50>: call 0x8048480 <_init+100>
0x080485d7 <main+55>: nop
0x080485d8 <main+56>: nop
0x080485d9 <main+57>: nop
0x080485da <main+58>: nop
0x080485db <main+59>: nop
0x080485dc <main+60>: nop
0x080485dd <main+61>: nop
0x080485de <main+62>: nop
0x080485df <main+63>: nop
--------------------------------------------------------------------------
分析到此结束。
总结:
用C++开发尽量使用优化选项,虚拟函数的调用除了要多几次给vptr赋值不同的vtable地址以外,效率和普通
函数是差不多的。类继承深度越长导致的性能问题在于要执行的基类的构造函数,需要初始化的数据成员越多,继承链
越长,调用的构造函数越多(inline后可以显著提高这部分的效率)。
4.与优化后C程序的对比
程序如下:
#include <stdio.h>
void Test();
int main()
{
Test();
}
void Test()
{
printf("Child::Test\n");
}
编译使用-O2优化,得到的汇编代码:
main函数:
Dump of assembler code for function main:
0x08048420 <main+0>: lea 0x4(%esp),%ecx
0x08048424 <main+4>: and $0xfffffff0,%esp
0x08048427 <main+7>: pushl 0xfffffffc(%ecx)
0x0804842a <main+10>: push %ebp
0x0804842b <main+11>: mov %esp,%ebp
0x0804842d <main+13>: push %ecx
0x0804842e <main+14>: sub $0x4,%esp
0x08048431 <main+17>: call 0x8048400 <Test>
0x08048436 <main+22>: add $0x4,%esp
0x08048439 <main+25>: pop %ecx
0x0804843a <main+26>: pop %ebp
0x0804843b <main+27>: lea 0xfffffffc(%ecx),%esp
0x0804843e <main+30>: ret
0x0804843f <main+31>: nop
End of assembler dump.
Test函数:
Dump of assembler code for function Test:
0x08048400 <Test+0>: push %ebp
0x08048401 <Test+1>: mov %esp,%ebp
0x08048403 <Test+3>: sub $0x8,%esp
0x08048406 <Test+6>: movl $0x80484cc,(%esp)
0x0804840d <Test+13>: call 0x80482b8 <_init+36>
0x08048412 <Test+18>: leave
0x08048413 <Test+19>: ret
0x08048414 <Test+20>: lea 0x0(%esi),%esi
0x0804841a <Test+26>: lea 0x0(%edi),%edi
End of assembler dump.
单单从汇编代码上面来看,C++的比C的多(异常处理的部分等等),C++使用jmp到打印函数的方法,而
C依然使用call的方式调用,在puts函数内部的处理,C++也应该做了一些特殊的处理(jmp进入,打印
完后直接退出程序,没有执行main函数中的出栈动作)。相比较于C而言,C++的优化是
另辟蹊径了。
以上结果使用
GCC 4.2.1
GAS 2.15
GDB 6.1.1
posted on 2010-03-07 23:25
Young
阅读(719)
评论(0)
编辑
收藏
引用
只有注册用户
登录
后才能发表评论。
【推荐】100%开源!大型工业跨平台软件C++源码提供,建模,组态!
网站导航:
博客园
IT新闻
BlogJava
知识库
博问
管理
导航
C++博客
首页
联系
聚合
管理
统计信息
随笔 - 13
文章 - 0
评论 - 1
Trackbacks - 0
常用链接
我的随笔
我的评论
我参与的随笔
留言簿
给我留言
查看公开留言
查看私人留言
随笔分类
FreeBSD(7)
(RSS)
LearnNote(1)
(RSS)
Nginx
(RSS)
STL(2)
(RSS)
随笔档案
2013年5月 (3)
2012年11月 (1)
2011年1月 (5)
2010年6月 (1)
2010年3月 (1)
2010年1月 (2)
文章分类
C++
(RSS)
FreeBSD
(RSS)
Nginx
(RSS)
自我管理
(RSS)
搜索
最新评论
1. re: 使用Bit Field的教训
更新好慢啊。。。
--ABC
阅读排行榜
1. C++中使用copy和ostream_iterator来输出map的内容(6341)
2. 用Qemu来调试FreeBSD内核(4659)
3. 使用VirtualBox来调试FreeBSD内核(4146)
4. FreeBSD9.1 下GDB7.5.1连接Qemu返回Remote ‘g’ packet reply is too long的问题解决(3699)
5. 给FreeBSD的Port下载提提速(3603)
评论排行榜
1. 使用Bit Field的教训(1)
2. [TCP/IP] DNS Review 1(0)
3. 使用VirtualBox来调试FreeBSD内核(0)
4. FreeBSD9.1 下GDB7.5.1连接Qemu返回Remote ‘g’ packet reply is too long的问题解决(0)
5. Sudo执行命令的重定向处理(0)
Powered by:
C++博客
Copyright © Young