由于有了上一节的铺垫,本节的内容相对较少,这里给大家准备了两个小文件来实例演示汇编语言和c语言相互调用。
会两种不同语言的人,只是掌握了同一件事物的两种表达方式。人在学习一种新语言时,潜意识里是建立了语言符号与事物形象的映射关系,比如我们在学习grape这个单词时,我们之所以认为它就是我们所认知的葡萄,是因为我们知道这两个名词都是在描述同一种圆圆的、黑紫色、甜酸的这一水果的形象,如果脑子中不存在这个形象的话,不光是学不会grape这个英文单词,就连中文的葡萄也不知道是何意。总之,对于具体的事物,一定是先有其形象,再有其描述,这样才能理解该事物,了解了事物的本质形象后,无论该事物的名字怎样变化,我们都能将它们相互转换。
也许有同学会问,以上这些所说的目的是什么?各位观众稍安勿燥,马上就要入戏啦^_^。
“汇编语言和C语言可以互相调用”,这句话并不是如表面陈述的那样,似乎是两种语言能直接交流,其实并不是这样。c语言和汇编语言完全是不同的东西,它们怎么能认识对方呢。这就像跟不懂汉语的人说汉语,那人听了肯定会晕头转向的,除非身边有个翻译帮忙转述,这个翻译所做的工作实质上是在脑子中找到这种语言所描述的事物形象,然后给出这种事物形象的另一种语言表达,这个事物形象才是翻译的核心。这有些类似上面提到的葡萄的例子,在同一种指令集上的各种计算机程序语言,最终也要编译为那些相同的机器码,这些机器码便是高级语言的本质形象。对于上面提到的翻译,在计算机世界里,就是编译器,只不过这个翻译是有多个,例如本书所说的c语言编译器gcc和汇编语言编译器nasm,它们能在一起配合,是因为它们都懂机器语言。举个例子,就像小明只会汉语和英语,小红只会汉语和法语,若他们之间在交流时,小明说英语,小红说法语,他俩相互都听不懂,所以,当说英文的小明想跟说法语的小红借作业时,他必须用汉语告诉小红。
编译器知道高级语言所描述的事物形象是机器码,所以各种编译在高级语言方面的交流,本质上都是将它们都变成统一的机器码后实现的。
吼吼,一不小心又说多了,不知道我表达清楚了没有,反正话题就此结束啦,小弟这里给各位看官准备了两个小文件:C_with_S_c.c和C_with_S_S.S。大家不用细看,快速浏览一下即可,在代码后面我还有话说呢
C_with_S_c.c
1 extern void asm_print(char*,int);2 void c_print(char* str) {3 int len=0;4 while(str[len++]);5 asm_print(str, len);6 }
C_with_S_S.S
1 section .data2 str: db "asm_print says hello world!", 0xa, 03 ;0xa是换行符,0是手工加上的字符串结束符\0的ascii码。4 str_len equ $-str56 section .text7 extern c_print8 global _start9 _start:10 ;;;;;;;;;;;; 调用c代码中的函数c_print ;;;;;;;;;;;11 push str ;传入参数12 call c_print ;调用c函数13 add esp,4 ;回收栈空间1415 ;;;;;;;;;;;;;;;;;;; 退出程序 ;;;;;;;;;;;;;;;;;;;;16 mov eax,1 ;第1号子功能是exit系统调用17 int 0x80 ;发起中断,通知linux完成请求的功能。1819 global asm_print ;相当于asm_print(str,size)20 asm_print:21 push ebp ;备份ebp22 mov ebp,esp23 mov eax,4 ;第4号子功能是write系统调用24 mov ebx, 1 ;此项固定为文件描述符1,标准输出(stdout)指向屏幕25 mov ecx, [ebp+8] ;第1个参数26 mov edx, [ebp+12] ;第2个参数27 int 0x80 ;发起中断,通知linux完成请求的功能。28 pop ebp ;恢复ebp29 ret
代码C_with_S_c.c中的函数c_print是被汇编代码C_with_S_S.S调用的,在c_print的实现中,它又调用汇编代码中的asm_print。它们的关系如图
下节再解释代码,先去吃饭了。
接上节,前文请见“一步步编写操作系统 69 汇编语言和c语言共同协作”,
本节是对前文的代码说明,代码再贴过来,C_with_S_c.c如下:
1 extern void asm_print(char*,int);2 void c_print(char* str) {3 int len=0;4 while(str[len++]);5 asm_print(str, len);6 }
代码C_with_S_c.c的第1行是声明外部函数asm_print,通知编译器这个函数并不在当前文件中定义。我们知道它定义在文件C_with_S_S.S中,但编译器是不知道的,所以只能在链接阶段将此函数重新定位,编排地址。
第2~6行是c_print的实现,在它的内部,它又调用了汇编代码C_with_S_S.S中的函数asm_print,经过第1行的声明,我们要给它提供两个参数:字符串的起始地址及长度。
特别强调一下,由于这里并不打算把c标准库也链接进来,所以在求字符串长度时,我们不能用string.h中的strlen函数。也就是说即使include <string.h>将其strlen的声明加进来,没有strlen实现本身所在的.o文件也是不行的。函数声明的作用是:一方面是告诉编译器该函数的参数所需要的栈空间大小及返回值,这样编译器能为其准备好执行环境,另一方面是如果该函数是在外部文件中定义的,一定要在链接阶段时将其对应的目标文件一块链接进来。所以这里第3~4行通过while循环求字符串的长度。字符串结尾必须是空字符’\0’才行,否则while就是死循环了。这个字符串是代码C_with_S_S.S提供的,我们转过去看看。
1 section .data2 str: db "asm_print says hello world!", 0xa, 03 ;0xa是换行符,0是手工加上的字符串结束符的ascii码。4 str_len equ $-str56 section .text7 extern c_print8 global _start9 _start:10 ;;;;;;;;;;;; 调用c代码中的函数c_print ;;;;;;;;;;;11 push str ;传入参数12 call c_print ;调用c函数13 add esp,4 ;回收栈空间1415 ;;;;;;;;;;;;;;;;;;; 退出程序 ;;;;;;;;;;;;;;;;;;;;16 mov eax,1 ;第1号子功能是exit系统调用17 int 0x80 ;发起中断,通知linux完成请求的功能。1819 global asm_print ;相当于asm_print(str,size)20 asm_print:21 push ebp ;备份ebp22 mov ebp,esp23 mov eax,4 ;第4号子功能是write系统调用24 mov ebx, 1 ;此项固定为文件描述符1,标准输出(stdout)指向屏幕25 mov ecx, [ebp+8] ;第1个参数26 mov edx, [ebp+12] ;第2个参数27 int 0x80 ;发起中断,通知linux完成请求的功能。28 pop ebp ;恢复ebp29 ret
在代码C_with_S_S.S的第2行,定义待打印的字符串时,在结尾人为的加了个0,它就是空字符’\0’的ascii码。
第8~9行是将_start导出为全局符号,为的是给链接器用的,之前解释过了。
为了在汇编文件中引用外部的函数(未必是c代码中的),需要用extern来声明所需要的函数名。
由于我们用到了外部的函数c_print,所以在第7行用extern c_print来声明c_print函数是外部的。第11~13行是为外部函数c_print压栈传参,调用它后清理栈空间。
第16~17行是调用exit系统调用告诉linux我要正常退出。
在汇编语言中导出符号名是用global关键字,这在之前说_start时大伙已有所耳闻,global是将符号导出为全局属性,对程序中的所有文件可见,这样其它外部文件中也可以引用被global导出的符号啦,无论该符号是函数还是变量。
由于在c代码文件C_with_S_c.c中也调用了这里的asm_print函数,所以为了让外部代码可以引用asm_print。我们在第19行用global asm_print将其导出。
第20行之后是asm_print的实现,相信大家已经非常明白了,不解释。
好啦,通过这两个例子我相信大伙儿已经掌握c和汇编混合编程的方法啦,确切说是方法之一。对于这种汇编代码和C代码单独编译的方式还是较容易的,以后咱们会讲C内联汇编的方式难为难为大家的,玩笑玩笑,只要用心没有学不会的(听上去似乎觉得更难了^_^,没事没事)。
有关混合编程的部分就说完了,总结一下:
- 在汇编代码中导出符号供外部引用是用关键字global,引用外部文件的符号是用关键字extern。
- 在C代码中只要将符号定义为全局便可以被外部引用(一般情况下无须用额外关键字修饰,具体请参考c语言手册),引用外部符号时用extern声明即可。
好了,大爷再来玩哦。