汇编友好的C编程技巧及其相互调用方法

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

【摘要】主要介绍如何优化C语言编程来产生最快速度和紧凑的机器指令汇编与C/C++混合编程的方法ARM C/C++编译器介绍

一、C语言编程优化

1. 尽量简化条件表达式

  • 在ARM指令体系中基本每个指令代码都有4位来表示条件码这类条件运算方式可以精简指令的代码。例如

    CMP	X, #0
    MOVGT y, #1
    MOVLT y, #0
    
    • 这样可以节省两条跳转指令和2.5个ARM时钟。
  • 在C语言编程中条件运算方式大都由if语句配合<、=、>等复杂操作转换而来但是在包含子函数及非常复杂的条件表达式时汇编器将不会将其转换为条件运算。所以为了精简指令和节省时间要尽量将if语句的条件简单化

2.在检查变量是否在某一范围时使用算数运算代替逻辑运算

  • 例如需要检查变量x是否处于min到max之间时一般采用如下形式的语句

    if(x >= min && x < max){
        ...
    }
    

    但若将其改用为:

    if((unsigned)(x-min) < (max-min)){
        ...
    }
    

    则编译后整个汇编代码的长度和执行时间会减少很多。

  • 这类表达式经常用在图像处理中判断某点是否处在某区域中。

3.循环语句的条件表达式尽可能递减循环

  • 在循环条件判断中尽量采用简单的运算可以节省很多指令。
  • 另外从n到0递减的循环要比从0到n的循环更节省指令和时间。

4. 尽量采用查表方式代替switch语句

  • 假设有如下switch语句

    char * conditionstr(int cond)
    {
        switch(cond){
            case 0: return "EQ";
            case 1: return "NE";
            case 2: return "CS";
            case 3: return "CC";
            case 4: return "MI";
            case 5: return "PL";
            case 6: return "VS";
            case 7: return "VC";
            case 8: return "HI";
            case 9: return "LS";
            case 10: return "GE";
            case 11: return "LT";
            case 12: return "GT";
            case 13: return "LE";
            case 14: return " ";
            default: return 0;
        }
    }
    

    可以采用如下查表方式来写这个语句

    char * conditionstr(int cond)
    {
        if((unsigned)cond >= 15)
            return 0;
        return "EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" + 3 * cond;
    }
    
    • 采用查表方式改写后编译后的代码由240字节降为72字节。

5. 善用函数声明

在ARM的C语言编程中可以使用一些函数声明来达到降低指令代码的执行效率。

1__value_in_regs

  • 将函数声明为==__value_in_regs==后函数返回数据可以是结构体从而节省程序代码的存储空间。例如

    typedef struct {int hi; uint lo;} int64;
    
    __value_in_regs int64 add64(int64 x, int64 y)
    {
        int64 res;
        res.lo = x.lo + y.lo;
        res.hi = x.hi + y.hi;
        if(res.lo < y.lo)
            res.hi++;
        return res;
    }
    

(2) __pure

  • 当一个函数的返回值与输入参数紧密相关时可以用函数声明__pure来精简系统的指令。例如

    __pure int square(int x)
    {
        return (x * x);
    }
    

(3) __inline

  • 当函数体短小且需要被频繁调用时可以使用内联函数声明这样可以省去调用函数反复切换上下文的开销时间上可以节省不少但会增加代码指令的长度。也就是说用空间换取时间。

    __inline int square(int x)
    {
        return (x * x);
    }
    

二、C语言与汇编混合编程

在实际便的编程应用中使用较多的是程序的开始部分初始化用汇编语言完成然后用C/C++程序完成主要的逻辑功能应用任务即程序在执行时首先完成汇编的初始化过程然后跳转到C/C++程序代码中。汇编程序和C语言程序之间一般没有太多参数传递和相互调用整个程序结构较为简单。根据结构不同他们分为以下3种情况

  1. 在C/C++代码中嵌入汇编指令
  2. 从汇编程序中访问C程序变量
  3. 汇编和C程序间相互调用

1. 在C/C++代码中嵌入汇编指令

  • ARM C语言中使用关键词==__asm{}==标识一段汇编指令程序

    __asm
    {
        ...	//汇编指令
    }
    
    • 若一行有多条汇编指令则用分号隔开。
    • 若一条汇编指令占用多行则用反斜杠\续行
    • 在汇编语句中可以使用C语言的注释语句
  • 在**ARM C++**语言中除了可以使用__asm_外还可以使用==asm()==来标识一段汇编指令程序

    asm("...");
    
    • asm括号中必须是一个单独的字符串不可使用注释语句
  • 在C/C++程序中使用内嵌汇编需共同注意的事项还包括

    • 因为在汇编语句中使用逗号作为变量间的分隔符所以若汇编指令中的C/C++表达式含有逗号则该表达式需要用括号括起来

    • 如果指令中使用了物理寄存器则需要保证该寄存器不会被编译器在计算表达式值时被破坏。例如

      __asm
      {
      	MOV r0, x
          ADD y, r0, x/y
      }
      
      • 上面的代码中因为计算x/y时使用到了r0寄存器所以第一条汇编语句的结果已被破坏得到的结果将是错误的。
      • 为此可以将r0换为C变量此后编译器将会自动为C变量分配一个合适的寄存器以避免被破坏。
    • 不要使用物理寄存器去引用一个C变量。例如

      int test(int x)
      {
          __asm
          {
              ADD r0, r0, #1
          }
          return x;
      }
      
      • 你以为进入函数后其参数x保存在r0寄存器中因而在内嵌汇编中直接使用r0代替x最后返回结果。

      • 但实际上编译器认为子程序没有做任何有意义的操作于是将该段汇编代码优化掉了从而返回的结果与输入的参数值相同并没有加1操作。正确的写法为

        int test(int x)
        {
            __asm
            {
                ADD x, x, #1
            }
            return x;
        }
        
    • 用户无需手动保存和恢复汇编器可能会用到的寄存器。

      • 对于内嵌汇编器可能会用到的寄存器编译器自己会保存和恢复用户不用保存和恢复它们。除常量寄存器CPSR和SPSR外其它寄存器必须先赋值后读取否则编译器报错。例如

        int fun(int x)
        {
            __asm
            {
                STMFD sp!, {r0}
                ADD r0, x, 1
                EOR x, r0, x
                LDMFD sp!, {r0}
            }
            return x;
        }
        
        • 第一条指令在还未给r0赋值前就读取它并保存起来这是错误的。
        • 最后一条指令恢复r0的值也是每必要的

2. 从汇编程序中访问C程序变量

在C程序中声明的全局变量可以被汇编程序通过地址间接访问具体有如下3种方法

  1. 使用IMPORT伪指令声明该全局变量
  2. 使用LDR指令读取该全局变量的内存地址一般位于程序的数据缓冲池中
  3. 根据该全局变量的数据类型使用相应的LDR指令读取其值使用相应的STR指令修改其值。
    1. 无符号char类型通过LDRB/STRB读写
    2. 无符号short类型通过LDRH/STRH读写
    3. int类型通过LDR/STR读写
    4. 有符号char类型通过LDRSB读取
    5. 有符号char类型通过STRB写入
    6. 有符号short类型通过LDRH读取
    7. 有符号short类型通过STRH写入
    8. 小于8个字的结构型变量通过LDM/STM读写
    9. 对于结构型变量的数据成员可以使用相应的LDR/STR指令访问此时需要知道该成员变量在结构体中的偏移量。
  • 综合举例

    AREA globals, CODE, READONLY
    EXPORT asmsub			;导出汇编函数使之可被其它文件中的代码调用
    IMPORT globvl			;声明C中的全局变量globvl
    asmsub					;汇编函数名标记
    LDR r1, =globvl			;将内存地址读入到寄存器r1中
    LDR r0, [r1]			;将globvl的值读入到寄存器r0中
    ADD r0, r0, #2
    STR r0, [r1]			;修改后再将值赋予变量
    MOV pc, lr
    END
    

3. 汇编和C程序间相互调用

汇编和C/C++相互调用时要特别注意遵守相应的ATPCS规则。下面分别予以说明

1C程序调用汇编程序

  • 在汇编中使用EXPORT伪指令声明本程序使得本程序可以被别的程序调用而在C语言中使用extern关键词声明该汇编程序。

  • 下面的例子是C代码调用汇编代码strcopy实现字符串复制工作

    //C程序
    #include <stdio.h>
    extern void strcopy(char *dststr, const char *srcstr);	//关键词extern声明汇编函数strcopy
    int main()
    {
        const char *srcstr = "First string of source";
        char dststr[] = "second string of destination";
        printf("%s\n%s\n",srcstr,dststr);
        strcopy(dststr, srcstr);	//调用汇编函数进行复制
        printf("After copying:\n");
        printf("%s\n%s\n",srcstr,dststr);
        return 0;
    }
    
    
    ;汇编程序
    AERA Scopy, CODE, READONLY
    EXPORT strcopy			;使用伪指令声明本汇编程序可被外部程序调用
    strcopy					;程序名r0存放dststr,r1存放srcstr
    LDRB r2, [r1], #1
    STRB r2, [r0], #1
    CMP  r2, #0
    BNE  strcopy
    MOV  pc, lr
    END
    

2汇编程序调用C程序

  • 汇编程序的设计要遵守ATPCS规则保证程序调用时参数的正确传递。

  • 在汇编程序中使用IMPORT伪指令声明将要调用的C程序。

  • 汇编调用C的例子

    //C程序g()返回5个参数的和
    int g(int a, int b, int c, int d, int e)
    {
        return a+b+c+d+e;
    }
    
    ;汇编调用C程序g()计算5个整数i,2i,3i,4i,5i的和前4个整数由r0至r3传递第5个由数据栈传递
    EXPORT
    AREA f, CODE, READONLY
    IMPORT g			;伪指令IMPORT声明C程序g()
    STR lr, [sp, #-4]!	;
    ADD r1, r0, r0		;假设进入程序f时r0的值为i,r1的值为2*i
    ADD r2, r1, r0		;r2的值为3*i
    ADD r3, r1, r2		;r3的值为5*i
    STR r3, [sp, #-4]!	;第5个参数5*i通过栈传递
    ADD r3, r1, r1		;r4的值为4*i
    BL  g				;调用C程序g()
    ADD sp, sp, #4		;调整数据栈指针准备返回
    LDR pc, [sp], #4	返回
    END
    

3C++程序调用C程序

  • C++程序调用C程序时在C程序中使用关键词==extern “C”==声明被调用的C程序。

  • 对于C++中的类或结构若没有基类和虚函数则相应对象的存储结构和ARM C相同。

  • 举例C++程序f()调用C程序cfunc()

    //被C++调用的C程序
    struct s{
        int i;
    };
    void cfunc(struct S *p)
    {
        p->i += 5;
    }
    
    //C++程序
    struct S	//本结构没有基类和虚函数
    {
        S(int S) : i(s){}
        int i;
    }
    
    extern "C" void cfunc(S*);	//使用关键词extern声明被调用的C程序
    int f()
    {
        S s(2);		//初始化该结构对象
        cfunc(&s);	//调用C函数
        return s.i * 3;
    }
    

4汇编程序调用C++程序

  • 在汇编程序中使用IMPORT伪指令声明将要调用的C++程序。

  • 在汇编程序中将参数存放在数据栈中而存放参数的数据栈单元地址放在寄存器r0中。

    //被汇编调用的C++程序
    struct S	//无基类和虚函数的结构
    {
        S(int S) : i(s){}
        int i;
    }
    
    void cppfunc(S *p)
    {
        p->i += 5;
    }
    
    ;调用C++程序的汇编程序
    AREA asm, CODE
    IMPORT cppfunc		;使用伪指令IMPORT声明被调用的C++程序
    EXPORT f
    f
    STMFD sp!, {lr}		;保存返回地址
    MOV r0, #2
    STR r0, [sp, #-4]!	;将实际参数存放在数据栈中
    MOV r0, sp			;将实际参数存放在数据栈中的地址存放在r0中
    BL cppfunc			;调用C++程序
    LDR r0, [sp], #4	;从数据栈中将结构读取到寄存器r0中
    ADD r0,r0,r0,LSL #1	;r0 = r0 * 3
    LDMFD sp!, {pc}		;返回
    END
    

三、ARM C/C++编译器

  • ARM中各种不同的C/C++编译器

    编译器名称编译器种类源文件种类源文件后缀输出的目标文件类型
    armccCC*.C32位ARM代码
    tccCC*.C16位Thumb代码
    armcppC++C/C++*.C/*.CPP32位ARM代码
    tcppC++C/C++*.C/*.CPP16位Thumb代码
  • ARM 编译器支持的对ANSI C进行扩展的关键词

    • __asm

      • 标识下面的代码是汇编代码主要在C语言程序中嵌套使用汇编语言
    • __inline

      • 让被声明的函数在合适的地方展开。但若该函数展开后很大则编译器也会将其作为一般函数对待。
    • __irq

      • 让被声明的函数用作IRQ或FIR异常中断的处理函数此时函数不仅保存默认的ATPCS标准要求的寄存器而且该函数还将lr-4的值赋给pc寄存器将spsr寄存器的值赋给cpsr寄存器从而实现函数的返回。
    • __pure

      • 被声明的函数的返回结果仅依赖其输入参数而不依赖除输入参数以外的任何数据。该函数编译后除了数据栈以外不访问任何其它存储单元。
    • __softfp

      • 表示声明的函数使用软件浮点连接件即传递给函数的浮点参数是通过整数寄存器传递的且若返回浮点数结果则也是通过整数寄存器传递的
    • __swi

      • 被声明的函数最多可以接受4个整数类参数最多可以利用value_in_regs返回4个结果。
    • __swi_indirect

      • 其使用格式如下

        int __swi_indirect(swi_num)
        swi_name(int real_num, int arg1,...);
        
        • swi_num为SWI指令中使用的SWI号。
        • real_num将通过寄存器r12传递给SWI处理程序这样就可以利用该参数存放要进行操作的编码。
    • __value_in_regs

      • 被声明的函数通过整数寄存器返回多达4个整数结果或者通过浮点寄存器返回返回多达4个浮点数/双精度结果。
    • __weak

      • 声明一个外部函数或对象。若连接器未找到该对象将不会报错并作为NULL处理或一个NOP指令。
  • 几个重要的ARM C/C++预定义的宏

    预定义宏名预定义宏值预定义宏生效场合
    _DATA_data编译源文件的日期
    _TIME编译源文件的时间
    _FILE_Name被编译文件的绝对路径名
    _func_Name当前被编译的函数名
    _LINE_Num当前被编译的代码行号
    _MODULE_Mod预定义宏_FILE_的文件名部分
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6