64位Ubuntu中C与intel汇编混合编程

这篇文章会介绍在64位的Ubuntu14.04下C语言和intel风格汇编语言的混合编程,使用gcc和nasm进行编译链接。

〇、目录

一、C语言内联汇编

1.引入示例

首先看一示例,它在C程序中嵌入intel风格的汇编语句,将变量b的值赋值给变量a,代码如下所示:

/* test.c */
#include<stdio.h>
int main() {
    int a=10, b=15;
    // the asm code is : a = b
    asm ("mov eax, %1;\n"
         "mov %0, eax;"
          :"=r"(a)
          :"r"(b)
          :"eax"
        );
    printf("a=%d,b=%d\n",a,b);
    return 0;
}

现在用gcc对它进行编译,参数-masm=[intel|att]用来选择英特尔或AT&T的汇编语法,
默认为att,这里选择了intel风格的汇编语法,参数-o test指明了输出文件的名字是test:

    gcc -masm=intel test.c -o test

然后运行它

    ./test 

输出

    a=15,b=15

若想查看编译结果,可以在执行gcc命令时添加参数-S,它会生成一个.s文本文件,其中是编译结果的汇编指令序列,注意添加-S命令后只生成.s文件,不再生成编译结果文件。

2.语法细节

C程序嵌入汇编有两种方式,一种是基本asm格式,此处不做介绍,另一种是上面示例中展示的扩展asm格式(Extended asm),其语法如下:

   asm [volatile](
        汇编语句模板
        : 输出部分
        : 输入部分
        :破坏描述部分
    );

asm表示汇编语句的开始,volatile是可选的,它会告诉编译器编译器不要对汇编指令进行优化,让它保持原样。
圆括号中的内容被三个冒号“:”分成了四个部分,其中汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”占位。

(1).汇编语句模板(assembler template)

简单地理解汇编语句模板就是汇编源程序,之所以叫做模板,是因为其中用到了%0,%1这样的操作数占位符,占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符来引用C语言变量。%i具体表示哪个C语言变量,则在输出部分和输入部分中确定。

当占位符引用的C语言变量不足32位时,会被扩展为32位。占位符表示汇编指令的操作数时,总被视为long型(4个字节,32位),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。

汇编指令语句之间使用“;”、“\n”或“\n\t”分开。

(2).输出部分(output operands)和输入部分(input operands)

输出、输入部分分别描述输出、输入操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C语言变量组成。每个输出操作数的限定字符串必须包含“=”,这表示它是一个输出操作数。限制字符有很多,参见常用限制字符表。描述符字符串表示对该变量的限制条件,这样Gcc就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令操作数与C表达式或C变量之间的联系。例如:

    "=r"(a)

表示将C语言变量a的值放入到某个通用寄存器中。

    "m"(n)

表示直接引用C语言变量n的地址。这里需要注意的是,gcc会用类似函数传参的方式在堆栈中取用n的地址,若占位符%0表示”m”描述的n,则%0可能翻译为[RBP-14048]这样的形式,那么%0[ESI][EBX]这样的基址加变址寻址的方式就是错误的了,它不等效于n[ESI][EBX],而是[RBP-14048][ESI][EBX]。

若直接引用变量的内存位置(限定字符串为”m”(n)等),则变量原本的类型会附带在汇编语句中,如int型的变量n用mov %0,ah是不行的,只能mov %0, eax。
char型的变量则可以用mov %0,ah,同理CMP BYTE PTR %0[RCX],0中的BYTE PTR是多余的。

若在64位的机器上进行内联汇编编译,使用32位的寄存器作为基址或变址寄存器可能会报错:

    XXX is not a valid base/index expression

若遇到这种情况,可以尝试改用64位的寄存器进行寻址,如将ESI改为RSI。

(3).破坏描述部分

破坏描述符用于告诉编译器我们会改变哪些寄存器或内存的值,由逗号格开的字符串组成。
如果在指令中存在某种不可以预见的访问内存方式的话,那么最好在此部分写上”memory”。

3.常用限制字符表(list of clobbered registers)

分类 限定符 描述
操作数类型 “=” 操作数在指令中是只写的(输出操作数)
操作数类型 “+” 操作数在指令中是读写类型的(输入输出操作数)
通用寄存器 “a” 将输入变量放入eax
通用寄存器 “b” 将输入变量放入ebx
通用寄存器 “c” 将输入变量放入ecx
通用寄存器 “d” 将输入变量放入edx
通用寄存器 “s” 将输入变量放入esi
通用寄存器 “q” 将输入变量放入eax,ebx,ecx,edx中的一个
通用寄存器 “r” 将输入变量放入通用寄存器
通用寄存器 “A” 把eax和edx合成一个64 位的寄存器(use long longs)
寄存器或内存 “g” 将输入变量放入eax,ebx,ecx,edx中的一个,或者作为内存变量
寄存器或内存 “X” 操作数可以是任何类型
内存 “m” 内存变量
内存 “o” 操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
内存 “V” 操作数为内存变量,但寻址方式不是偏移量类型
内存 “p” 操作数是一个合法的内存地址(指针)
立即数 “I” 0-31之间的立即数(用于32位移位指令)
立即数 “J” 0-63之间的立即数(用于64位移位指令)
立即数 “N” 0-255之间的立即数(用于out指令)
立即数 “i” 立即数
立即数 “n” 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
匹配 “0” 表示用它限制的操作数与某个指定的操作数匹配
匹配 “1” 也即该操作数就是指定的那个操作数,例如“0”
匹配 & 该输出操作数不能使用过和输入操作数相同的寄存器

二、C语言调用汇编子函数

1.综述

在Linux中无法使用masm,我想编译intel风格的汇编源程序,所以选择使用nasm。(AT&T风格的汇编源程序可以用as命令编译。)
nasm和masm语法相似但亦有不同之处,具体请查阅资料。

假设在test.c中调用test.asm中的函数,那么:

在test.asm文件中,引入外部变量、函数需要用关键字extern,不用说明数据类型。导出函数需要用关键字global。

在test.c,共享的变量需要定义在主函数main之外,从外部引入的函数需要用关键字extern说明。

在64位机器中,gcc编译默认是在64位模式下的,而nasm则默认是在32位下的,若将两者的编译结果进行链接,则会出错。
解决方法之一是让nasm工作在64位模式下:

    nasm -f elf64 test-s.asm

参数-f elf64表示生成64位的elf文件。这一命令会生成test-s.o。

而用gcc的编译C源程序的命令是:

    gcc -c test-c.c

参数-c表示只编译不链接。这一命令会生成test-c.o。

再进行链接,依旧使用gcc:

    gcc test-s.o test-c.o -o test

参数-o test指定了输出文件名为test,注意此文件名没有后缀。

2.示例

fun是定义在test-s.asm中的子函数,参数是整数n和字符a,该函数的功能是修改全部变量buf的第n个字符为a。
test-c.c会以3和’a’为参数调用它。调用fun前后buf的值应该分别是”test”和”teat”。源程序如下所示:

/* test-c.c */
#include<stdio.h>
//引入外部函数
extern   void fun(int, char);
//共享的变量申明必须在主函数外
char buf[]="test";
int main() {
    char a='a';
    int  n=3;
    printf("The old buf is %s.\n",buf);
    fun(n, a);    //调用汇编程序写的子函数fun。
    printf("The new buf is %s.\n",buf);
    return 0;
}
;test-s.asm
;引入C语言中变量
extern buf;
[section .text]         ;代码段
global fun               ;导出函数fun
fun:
    mov    rax, rsi
    mov    [buf+rdi-1], al
    ret

由于x86_64体系架构中函数调用时整数和指针参数按照从左到右的顺序依次保存在寄存器rdi,rsi,rdx,rcx,r8和r9中,浮点型参数保存在寄存器xmm0,xmm1等中,若有更多的参数则按照从右到左的顺序依次压入堆栈。所以上例中rdi中保存了参数n的值,rsi中保存了参数a的值。

对它们进行编译链接并执行,(”~$ “开头的一行表示是命令)如下所示:

    ~$ nasm -f elf64 test-s.asm
    ~$ gcc -c test-c.c
    ~$ gcc test-s.o test-c.o -o test
    ~$ ./test
    The old buf is test.
    The new buf is teat.

3.扩展:Windows下CodeBlocks与Gcc编译器

对于linux用户来说,用gcc编译程序是方便而又经常的事,而对于Windows用户来说,则可能更习惯于使用集成开发环境,而不直接接触编译器。其实Windows中也可以简单的开始使用Gcc编译器编译程序。

当然直接下载Gcc编译器安装运行也是很简单的,它是遵循GPL协议的自由软件。但实际上可能不需要这么麻烦,如果你已经安装了Code::Blocks,那么你很可能已经拥有了Gcc编译器,而且很可能长久以来,你一直在使用它编译程序。
打开文件夹C:\Program Files (x86)\CodeBlocks\MINGW\bin\,其中C:\Program Files (x86)\是CodeBlocks的目录,你的可能和我不一样。(如果你不了解自己的CodeBlocks安装在哪里,可以打开CodeBlocks,选择Setting -> Debugger -> Default,其中的Executable path就是我刚刚给出的文件夹。)看看其中是否有gcc.exe,若果有,那么恭喜你,不用再额外安装它了,若没有,则请下载安装。

现在假设你在C:\Program Files (x86)\CodeBlocks\MINGW\bin\找到了gcc.exe,接下来是如何使用它的问题——设置环境变量。打开计算机->属性->高级系统设置->高级->环境变量->Path,编辑它,在其末尾添加C:\Program Files (x86)\CodeBlocks\MINGW\bin\gcc.exe;之后保存退出就可以了。当然这是我的路径,你应当替换为你的。之后打开cmd命令行窗口,输入gcc,若显示gcc: fatal error: no input files之类的语句则表明设置成功,若提示该命令不存在等等则说明以上某一步或几步出错了,请耐心地检查。

三、参考

  1. GCC嵌入式汇编简介
  2. GCC扩展内联汇编
  3. AT&T 汇编和 GCC 内联汇编简介
  4. gcc下对汇编最好的处理文章____assembly______
  5. gcc内联汇编函数语法 
  6. 内联汇编 – 从头开始
  7. GCC内联汇编基础
  8. GCC内联汇编 
  9. gcc的内联汇编取全局变量地址
  10. C++为何内联汇编找不到定义的变量?
  11. NASM x86汇编入门指南
  12. 关于C语言和汇编语言混合编程的一点思考
  13. NASM入门教程(part1)
  14. NASM汇编HelloWorld
  15. GCC 和 NASM 联合编译,汇编函数前要有引到下划线 _
  16. Compiling C
  17. ld: i386 architecture of input file `hello.o’ is in
  18. gcc的mtune和march选项分析
  19. x86_64体系结构函数调用时函数参数传递方法
  20. Linux assemblers: A comparison of GAS and NASM
  21. nasm 与 masm语法区别
  22. NASM Assembly Language Tutorials – asmtutor.com
  23. Unix下NASM之和C语言互相调用
  24. 学习 nasm 语言
  25. 为什么在nasm中mov指令往内存移数据只能按字的方式
  26. 64位模式下 nasm 和c语言的互相调用

3 Replies to “64位Ubuntu中C与intel汇编混合编程”

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

15 + 5 =