MIPS架构深入理解11-向MIPS移植软件之编程语言

2022-08-15 16:26:40 浏览数 (1)

站在巨人的肩膀上,才能看得更远。 If I have seen further, it is by standing on the shoulders of giants. 牛顿

这是向MIPS架构移植软件的问题系列之第四篇。在前三篇文章

*《MIPS架构深入理解8-向MIPS架构移植软件之大小端问题》

*《MIPS架构深入理解9-向MIPS移植软件之Cache管理》

*《MIPS架构深入理解10-向MIPS移植软件之内存序》

中,我们分别讨论了大小端模式、Cache和内存序对于移植代码的影响。那么本文,我们再从编程语言的角度,思考一下移植代码时应该注意的事项,尤指底层代码或操作系统代码。

大部分编程人员,可能习惯了C或C 语言,而MIPS架构缺乏特殊的I/O操作指令。这意味着,要想访问I/O寄存器,只能使用load或者store之类的指令,通过恰当的操作来实现。但是,I/O寄存器的访问有一些限制,因此,必须确保编译器不能太聪明,编译出了违背我们意愿的结果。另外,MIPS架构使用了大量的CP0寄存器,我们也可以使用C语言的伪汇编asm()方法进行操作。

1 封装汇编代码

对于GCC编译器,几乎是家喻户晓,其允许在C文件中封装汇编代码。当然了,其它编译器也支持,只是语法上不同罢了。在这儿,我们只以GCC进行举例;至于其它的编译器,请自行google或者baidu。如果,想要写一个高效计算的库函数之类的,可以使用纯MIPS汇编语言进行编写;但是,如果只是想在某个C文件中,插入一小段汇编语言,可以使用asm()伪指令实现。甚至,你可以让编译器根据一些约定,自行选择使用的寄存器。

比如说,下面的这段代码,调用乘法指令mul,就可以在绝大数的MIPS架构CPU上运行。我们可以注意到,mul指令后面跟着三个源操作数。如果我们直接使用C语言的*乘法操作符,生成的乘法汇编指令一般只使用两个操作数,而且隐含地将生成的double类型的结果保存到hi/lo寄存器中。

下面这段伪汇编代码实现的mymul乘法函数,使用了三目乘法指令mul,只保存double型结果的低有效部分到p变量中,高有效部分被抛弃。由我们自己决定如何避免溢出或者其它不相干的事情。

代码语言:javascript复制
static int __inline__ mymul(int a, int b)
{
    int p;

    asm(
        "mul %0, %1, %2"
        : "=r" (p)
        : "r" (a), "r" (b)
    );

    return p;
}

函数本身被声明为inline内联函数,这意味着应该使用该函数逻辑代码的拷贝去替代调用这个函数的地方的代码(这允许局部寄存器优化)。使用static进行限定,不允许其它模块文件调用该函数,所以,不会生成这个函数本身的二进制代码。封装asm()代码时,经常会这样干。然后,将这个伪汇编代码放到某个include文件中。当然,也可以使用C语言预处理宏来进行定义,但是,使用inline函数更简洁一些。

上面的代码,告知GCC,传递给汇编器一个MIPS的mul指令,具有三个操作数,一个是输出,两个是输入。

%0的意思就是指向索引为0的变量,也就是p。首先,我们使用=修改符指明这个值是write-only的;其次,通过符号r告诉GCC,可以自由选择任何一个通用寄存器保存这个值。

asm()中的第3行代码,告诉GCC,操作数%1%2分别是ab,并且允许GCC将其保存到任何通用目的寄存器中。

示例函数的最后,就是表明,把结果返回给调用者。

从上面的示例可以看出,GCC允许对操作数进行相当自由的控制。你可以告诉某个值可读可写,某些寄存器可能会留下毫无意义的值等。详细的使用方法可以参考GCC手册中关于MIPS架构的部分章节内容。

2 内存映射的I/O寄存器和volatile

因为在MIPS架构中,将所有的I/O寄存器映射到内存上,可以很容易使用C语言编写代码进行访问。所以,不到迫不得已,不要使用汇编语言操作这些I/O寄存器。我们已经说过,随着编译器的发展,或者在你的代码中使用了大量的C 代码,很难预测最终生成的汇编指令的顺序。下面我们将再谈论一些老生常谈的问题。

下面是一段代码,用来轮询串口的状态寄存器。如果准备就绪,就发送一个字符:

代码语言:javascript复制
unsigned char *usart_sr = (unsigned char *) 0xBFF00000;
unsigned char *usart_data = (unsigned char *) 0xBFF20000;
#define TX_RDY 0x40
void putc (ch)
char ch;
{
    while ((*usart_sr & TX_RDY) == 0);
    *usart_data = ch;
}

这段代码,编译器很可能将映射到内存上的寄存器变量usart_sr,视作一个不变的变量;而在while循环中也没有存储按位与表达式的结果的地方,编译器可能会自作主张的将其保存到一个临时变量中。最终,上面的代码可能等效于下面的代码。结果可能就是一直发送某个字符,也可能一直无法输出。

代码语言:javascript复制
void putc(ch)
char ch;
{
    tmp = (*usart_sr & TX_RDY);
    while (tmp);
    *usart_data = ch;
}

为了避免这种情况,我们必须让编译器意识到,usart_sr是一个随时变化的值的指针,不能被优化。方法就是添加限定符volatile,如下所示:

代码语言:javascript复制
volatile unsigned char *usart_sr =
(unsigned char *) 0xBFF00000;
volatile unsigned char *usart_data =
(unsigned char *) 0xBFF20000;

相似的情况,也可能发生在中断或者异常处理程序中要修改的变量身上。同样的,可以使用volatile进行限定。但是,你需要避免像下面的代码那样使用volatile

代码语言:javascript复制
typedef char * devptr;
volatile devptr mypointer;

本意是想告诉编译器,重新从char *类型的指针处加载数值,但是使用上面的方式,没有起到任何作用。应该如下所示,进行声明:

代码语言:javascript复制
typedef volatile char * devptr;
devptr mypointer;

通过上面的讨论过程,我们可以看出使用C编写驱动程序要更容易一些,代码的阅读性也更好。但是,你需要充分理解硬件行为和工具链生成机器指令的方式,保证系统按照想要的行为进行工作。

3 在MIPS架构上使用C编写程序时的一些其它问题

  • 负指针 当在MIPS架构上运行比较简单的程序时,一般直接运行在非映射内存区,也就是kseg0kseg1区域时,所有32位数据指针的最高位都置1,看起来像是一个负数。而在其它架构上,运行这种程序一般都在低于2G的内存地址上,也就是直接对应物理地址。所以,MIPS架构的这种负指针,如果对其进行比较运算的话,指针可能会隐式地被转为一个有符号的整数类型。所以,在进行指针和某个整数进行比较的时候,一定要显式地指定为无符号整数类型,比如unsigned long。大部分的编译器都会对指针向integer类型进行转换时给出警告。
  • 有符号与无符号字符类型 早期的C编译器,char类型一般用于string,通常是signed char类型;这与为了获取更大整数值的约定是一致的。但是,当处理超过127的字符编码时,比如转换或者比较,就会很危险。现代编译器一般都将char型等同于unsigned char类型。如果发现你的旧代码依赖于char类型的默认符号扩展,一定检查编译器是否有选项,恢复这个传统的约定。
  • 16位int类型数据的使用 当我们从16位的机器架构的程序,比如x86或者ARM等,移植到MIPS架构上时,一定要注意最大值、溢出和符号位扩展。笨方法就是,直接将这些程序的int型替换成short类型,但这需要时间和耐心

    0 人点赞