现在的位置: 首页 > 综合 > 正文

转C/C++和汇编混合编程

2013年08月29日 ⁄ 综合 ⁄ 共 4492字 ⁄ 字号 评论关闭

转C和汇编混合编程

.data是初始化的数据块。这些数据包括编译时被初始化的globle和static变量,也包括字符串。
连接器将OBJs及LIBs文件的.data结合成一个大的.data。local变量以放在一个线性的堆栈中,
不占.data和.bss的空间。和.text一样,数据块是以明文的形式存放在文件中的。无法防止对其物理的修改。
.bss区是存放未初始化全局和静态变量的。

在C和汇编混合编程的时候,存在C语言和汇编语言的变量以及函数的接口问题。
在C程序中定义的变量,编译为.asm文件后,都被放进了.bss区,而且变量名的前面都带了一个下划线。在C程序中定义的函数,编译后在函数名前也带了一个下划线。例如:

extern int num就会变成 .bss _num, 1
extern float nums[5]就会变成.bss _nums, 5
extern void func ( )就会变成 _func,

 函数声明: C中在函数前加extern声明此函数为外部函数,在汇编中要声明函数名为全局变量,如:
   extern void delay(void) ;  /*in C*/
   globl delay   ; in asm
 _delay:   ; delay function begins

(1) 汇编程序中访问c程序中的变量和函数。
在汇编程序中,用_XX就可以访问C中的变量XX了。访问数组时,可以用_XX+偏移量来访问,如_XX+3访问了数组中的XX[3]。

    在汇编程序调用C函数时,如果没有参数传递,直接用_funcname 就可以了。如果有参数传递, 则函数中最左边的一个参数由寄存器A给出,其他的参数按顺序由堆栈给出。返回值是返回到A寄存器或者由A寄存器给出的地址。同时注意,为了能够让汇编语言能访问到C语言中定义的变量和函数,他们必须声明为外部变量,即加extern 前缀。

(2) c程序中访问汇编程序中的变量

    如果需要在c程序中访问汇编程序中的变量,则汇编程序中的变量名必须以下划线为首字符,并用global使之成为全局变量。

    如果需要在c程序中调用汇编程序中的过程,则过程名必须以下划线为首字符,并且,要根据c程序编译时使用的模式是stack-based model还是register argument model来正确地编写该过程,使之能正确地取得调用参数。

(3) 在线汇编

    在C程序中直接插入 asm(“ *** ”),内嵌汇编语句,需要注意的是这种用法要慎用,在线汇编提供了能直接读写硬件的能力,如读写中断控制允许寄存器等,但编译器并不检查和分析在线汇编语言,插入在线汇编语言改变汇编环境或可能改变C变量的值可能导致严重的错误。

汇编和C接口中寻址方式的改变:

    需要注意的是,在C语言中,对于局部变量的建立和访问,是通过堆栈实现的,它的寻址是通过堆栈寄存器SP实现的。而在汇编语言中,为了使程序代码变得更为精简,TI在直接寻址方式中,地址的低7位直接包含在指令中,这低7位所能寻址的具体位置可由DP寄存器或SP寄存器决定。具体实现可通过设置ST1寄存器的CPL位实现,CPL=0,DP寻址,CPL=1,SP寻址。在DP寻址的时候,由DP提供高9位地址,与低7位组成16位地址;在SP寻址的时候,16位地址是由SP(16位)与低7位直接相加得来。

    由于在C语言的环境下,局部变量的寻址必须通过SP寄存器实现,在混合编程的时候,为了使汇编语言不影响堆栈寄存器SP,通常的方式是在汇编环境中使用DP方式寻址,这样可以使二者互不干扰。编程中只要注意对CPL位正确设置即可

 

转C++与汇编混合编程

C/C++都是非常贴近最终汇编实现的语言,它们可以很容易地和汇编共存。通常一个函数会被编译成一段汇编代码,函数指针就是指向这段代码的地址。函数参数是通过把数据压入堆栈传递给函数的,在C++里,为了提高效率,通常会把this指针置入ECX寄存器来传入函数,因为寄存器的访问速度要优于内存。而函数在运行时,编译器产生的代码通常并不会用POP指令将传入的参数出栈,而是修改EBP的值,指向当前堆栈,然后通过对EBP的间接寻址来访问参数,这样,在多层函数调用中,使得每层函数的参数都得到保护。同样,函数用到的局部变量也会被放入堆栈中,通过EBP来读些。当函数返回时,C语言的默认做法是让调用者去处理传入的参数占据着堆栈的问题,而C++的默认做法是函数本身从堆栈中去掉这些参数。
函数的返回值一般会放入EAX寄存器中,某些情况会用到EDX。如果返回的数据过大,比如是一个类或结构,许多编译器的做法是把返回值需要的数据空间有调用者在堆栈中分配好,把这个临时变量的指针传入函数,函数把结果填入指针指向的内存。
我们自己写汇编代码时如果想调用一个C++语言函数,则在调用前,如果EAX,ECX,EDX内的数据对你的程序上下文有意义,那么请自己提前保存起来(C语言可以不保存ECX),而EBX,EDI,ESI,EBP这些寄存器的数据不会被破坏,ESP,EIP则会按约定的逻辑起变化。
同样如果想用汇编写一个可供C++调用的函数,那么请遵守同样的约定。你的函数如果用到EBX,ESI,EDI等寄存器,请在函数入口处保存,并在函数返回前恢复。而EAX,ECX,EDX则可以任意使用(C环境下,ECX例外,需要得到和EBX一样的保护)。

 

 

转VC7中汇编和C++混合的初步心得

1 查看编译器输出

通常来说,Debug 模式单步跟踪时Alt+8 就可以看见汇编代码。问题是 Debug 只是代表了一个侧面,并不代表最终的 Release ;另一方面 Debug 模式包含了些许额外的测试代码 —— 恩,可能代码有些多...天啊,他们干嘛要加、那么多、莫名其妙的代码混淆视听阿!

好嘛,看看简洁的Release模式 —— orz.... 不能单步跟踪C++程序了? 连main函数在哪里都看不见... 瞎了...

Release 模式单步跟踪要需要高深的技术底气。不过也没那么绝,要看 Release 模式的输出,我们可以在项目属性->C/C++->输出文件页面中把“汇编输出”项定为“带源代码的程序集(/FAs)”。这样,在Release目录下就可以看见对应的asm文件了。看asm文件,唯一的缺点是不能单步跟踪研究。

这个asm文件搞不好会非常大——主要是由于C++标准库广泛使用模板的原因,若我们放弃C++库一律使用C标准库就会看到很干净的asm文件(同时会看见一个1/4大小的可执行文件,你会明白为什么那么多人支持C )——当然这不是C++的干活。 要在这个动辄数W行的文件中里面找源代码对应的汇编,推荐大家找一行一定不会被优化掉的代码(没错,某些代码可能人间蒸发),直接F3搜索。

asm中包含了很多注释,有基本的汇编知识然后连蒙带猜就能看懂了。一对挺有用的标志是:

_TEXT SEGMENT // 代码段开始标志
_TEXT ENDS // 代码段结束标志

对于观察每个函数的生成代码来说,这两个标志能起到路标的作用。

 

2 汇编访问类成员

若有一个类

class A{
int _i;
};

有A 的实例a,下面的代码令 a._i = 10,这只需要一个指令:

__asm mov [a]A._i, 10

但是在A 的成员函数中怎么办呢?

我们知道,成员函数调用为 thiscall, this通过 ecx传递。所以在函数的开头现场尚未被破坏的时候,可以直接用 ecx 变址访问。如下面是一个常见的set函数, 它令 A::_i = n (注意mov等指令中,两个操作数不能同时为内存内容,所以必须用寄存器eax接力):

inline void A::i( int n ){
__asm mov eax, n
__asm mov [ecx]A._i, eax
}

不过这有两个问题。一来,ecx并非总是this;它随时可能被刷掉。在某个不能确定保存this寄存器的时候,你需要手动写ecx:

__asm mov ecx, this
__asm mov eax, n
__asm mov [ecx]A._i, eax

这样写会迫使编译器把this的值复制到栈上 —— 而一般来说对于小函数而言,编译器会尽量只用寄存器。这可能是一个额外的小小开销。(注意,千万不要以为可以这么访问: [this]A._i )

另一方面,虽然在我们的确写了大大的“ inline ”几个字,但是看看输出代码——你会发现:任何包含了内嵌汇编的 inline 成员函数都不会被内联!

 

3 汇编/内联函数和效率

普通函数是可以内联的,下面就是一个完美的结合 C++/ asm 的例子:

inline long long getTimer(){
long long time;
__asm rdtsc
__asm mov DWORD PTR time, eax
__asm mov DWORD PTR time + 4, edx
return time;
}

rdtsc指令用来获得CPU自开机运行的时钟周期数。它的结果是64位的,保存到 eax 和 edx两个寄存器中,可以用来精确测量算法开销。上面的函数内联之后, 局部变量不见了, 临时返回值也不见了,只有最核心的三行代码,没有比这更简洁的了:

; 68 : long long b = getTimer();

rdtsc
mov DWORD PTR _time$11298[ebp], eax
mov DWORD PTR _time$11298[ebp+4], edx

成员函数内联则又是另一个故事:系统不知道如何处理this,所以他干脆忽略所有内嵌asm成员函数的内联标志。

好嘛,VC不愿上,我们用皮鞭赶着他上! 把第二部分最初那个 A::i 改为 __forceinline 就强制内联了——也就是强制VC犯错误了:不幸的编译器看不懂我们的代码,只好把指令抄到函数调用处。他不晓得初始化ecx,那个mov可能往任何地方写内容——比如把你的开机密码写到桌面上——

虽然可以手动设置ecx,不过我们可不希望看见如此丑陋的调用(想象一下你的同事看到这段代码的困惑):

__asm lea ecx, a
a.i( 20 );

要正确编写能成功内联的代码必须结合另一个方案,手动复制this:

__forceinline void A::i( int n ){
__asm mov ecx, this
__asm mov eax, n
__asm mov [ecx]A._i, eax
}

厄。。。猜猜看结果如何?

首先看看我们直接用C++写一个 set函数 (譬如 void A::i( int n ){ _i = n; } )内联后的结果:

; 56 : a.i( 5 );

mov DWORD PTR _a$[ebp+8], 5

最残酷的结果也只需一句mov。 更可能的结果是——他被优化得连影儿都看不见。
然后看看我们的三年怀胎含辛茹苦研究出来的混合汇编的内联:

; 56 : a.i( 5 );

lea eax, DWORD PTR _a$[ebp]
pop ecx
mov DWORD PTR $T11194[ebp], eax
mov ecx, DWORD PTR $T11194[ebp]
mov eax, 5
mov DWORD PTR [ecx+8], eax

这么长啊....生出一个怪胎... VC 中嵌入汇编的一个坏处是:编译器很难将他和C++协调,很难优化他。

汇编优化可以很快速、很强,但是一定要慎用。

抱歉!评论已关闭.