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

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

站在巨人的肩膀上,才能看得更远。

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变量中,高有效部分被抛弃。由我们自己决定如何避免溢出或者其它不相干的事情。

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++代码,很难预测最终生成的汇编指令的顺序。下面我们将再谈论一些老生常谈的问题。

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

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循环中也没有存储按位与表达式的结果的地方,编译器可能会自作主张的将其保存到一个临时变量中。最终,上面的代码可能等效于下面的代码。结果可能就是一直发送某个字符,也可能一直无法输出。

void putc(ch)
char ch;
{
    tmp = (*usart_sr & TX_RDY);
    while (tmp);
    *usart_data = ch;
}

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

volatile unsigned char *usart_sr =
(unsigned char *) 0xBFF00000;
volatile unsigned char *usart_data =
(unsigned char *) 0xBFF20000;

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

typedef char * devptr;
volatile devptr mypointer;

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

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类型,但这需要时间和耐心😊。大部分时候,可以直接使用MIPS架构的32位int类型替换。但是,需要特别注意的是signed类型比较时的bit16的溢出问题。
    还有就是,使用两个16位整型数拼凑成一个32位整型数时,一定要使用无符号16位整型数。笔者在移植ARM架构的操作系统到MIPS架构上时,就是使用了signed short类型的2个变量拼接成一个32位整数时,由于符号位扩展的原因(高16位全部被填充为1)导致高位数一直无法生效。
  • 堆栈的使用
    尽管MIPS架构缺乏对堆栈的支持,但是MIPS-C编译器还是实现了一个常规的栈结构,主要就是按照某种约定,指定通用寄存器作一些特殊的用途,比如使用哪几个寄存器传递函数参数,使用哪个寄存器作为stack指针寄存器等等。话虽如此,不要想当然的认为,堆栈就可以安全的移植了。必要的时候,使用下面的2个方法-宏和库函数-解决堆栈的问题:
  • stdargs:
    使用头文件,定义宏,允许函数接收可变参数。
  • alloca():
    使用这个函数动态分配内存。有些编译器实现alloca()为内嵌函数,来扩展堆栈;也可以使用单纯的库函数实现。但是,不要假设堆栈和其分配的内存有什么关系。
相关文章
|
1月前
|
运维 负载均衡 Shell
控制员工上网软件:高可用架构的构建方法
本文介绍了构建控制员工上网软件的高可用架构的方法,包括负载均衡、数据备份与恢复、故障检测与自动切换等关键机制,以确保企业网络管理系统的稳定运行。通过具体代码示例,展示了如何实现这些机制。
123 63
|
27天前
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
101 8
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
|
4月前
|
人工智能 运维 虚拟化
完善多云平台软件体系,VMware再探索下一代企业IT架构
完善多云平台软件体系,VMware再探索下一代企业IT架构
|
1月前
|
Kubernetes 前端开发 分布式数据库
工作中常见的软件系统部署架构
在实际应用中,会根据项目的具体需求、规模、性能要求等因素选择合适的部署架构,或者综合使用多种架构模式来构建稳定、高效、可扩展的系统。
186 2
|
4月前
|
边缘计算 物联网 5G
软件定义网络(SDN)的未来趋势:重塑网络架构,引领技术创新
【8月更文挑战第20天】软件定义网络(SDN)作为新兴的网络技术,正在逐步重塑网络架构,引领技术创新。随着5G、人工智能、边缘计算等技术的不断发展,SDN将展现出更加广阔的应用前景和市场潜力。未来,SDN有望成为主流网络技术,并在各行各业推动数字化转型。让我们共同期待SDN技术带来的更加智能、安全和高效的网络体验。
|
4月前
|
消息中间件 Kafka Java
Spring 框架与 Kafka 联姻,竟引发软件世界的革命风暴!事件驱动架构震撼登场!
【8月更文挑战第31天】《Spring 框架与 Kafka 集成:实现事件驱动架构》介绍如何利用 Spring 框架的强大功能与 Kafka 分布式流平台结合,构建灵活且可扩展的事件驱动系统。通过添加 Spring Kafka 依赖并配置 Kafka 连接信息,可以轻松实现消息的生产和消费。文中详细展示了如何设置 `KafkaTemplate`、`ProducerFactory` 和 `ConsumerFactory`,并通过示例代码说明了生产者发送消息及消费者接收消息的具体实现。这一组合为构建高效可靠的分布式应用程序提供了有力支持。
120 0
|
4月前
|
监控 持续交付 数据库
持续交付的软件系统架构
持续交付的软件系统架构
41 1
|
4月前
|
测试技术
软件设计与架构复杂度问题之区分软件维护、演进和保护(苟且)如何解决
软件设计与架构复杂度问题之区分软件维护、演进和保护(苟且)如何解决
|
4月前
|
微服务
软件设计与架构复杂度问题之理解软件复杂性的递增性如何解决
软件设计与架构复杂度问题之理解软件复杂性的递增性如何解决
|
4月前
|
Serverless 微服务
软件设计与架构复杂度问题之ady Booch描述软件的复杂性如何解决
软件设计与架构复杂度问题之ady Booch描述软件的复杂性如何解决