架构这个词,英文是architecture,牛津词典对其解释为the design and structure of a computer system
。所以,这个词体现的是设计和结构,也就是说,是一个抽象机器或通用模型概念上的描述,而不是一个真实机器的实现。这就好比一辆手动挡车,无论是前轮驱动还是后轮驱动,它的油门总是在右,离合器在左。这里,油门和离合器的位置就相当于架构,前轮还是后轮驱动是具体实现。所以,相同的架构,实现未必相同。
当然了,如果你是一个拉力赛车手,在湿滑的路上高速行驶时,前轮驱动还是后轮驱动就很重要了。计算机也是一样,如果对某个方面有特殊的需求,实现的细节就很重要了。
通常,CPU架构由指令集和寄存器组成。术语-指令集和架构在语义上非常接近,所以,有时候你也会见到这两个词的组合缩写-指令集架构(ISA)。
对于MIPS指令集架构描述最好的,肯定是MIPS公司出版的MIPS32和MIPS64架构规范。MIPS32是MIPS64的一个子集,用于描述具有32位通用目的寄存器的CPU。为了简单,我们缩写为MIPS32/64
。
生产MIPS架构CPU的公司,尽量兼容MIPS32/64
规范。
在MIPS32/64
规范之前,已经发布了多版的MIPS架构。但是,这些旧架构只是规定了软件使用的指令和资源,并没有定义操作系统所需要的CPU控制机制,而是将其认为应该在实现时定义。通俗地讲,早期版本的MIPS架构对CPU控制单元的硬件实现不做约束,由芯片制造商在实现时自己实现。这意味着,对于可移植操作系统需要做更多的工作,去适配因此而带来的差异。好消息是,几乎每一个版本的MIPS架构,都有一个作为所有实现的父版本
存在。
单精度对(paired-single)
-出现。Silicon Graphics
公司分拆出来的MIPS Technologies Inc.
公司制定的标准。该标准第一次纳入了CPU控制的功能,由协处理器0实现。MIPS32是MIPS-II的超集,MIPS64是MIPS-IV的超集(还以可选的方式包含了MIPS-V的大部分)。
大多数1999年之后设计的MIPS架构CPU都兼容这些标准。所以,在后面的描述中,我们使用MIPS32/64
作为基础架构。到目前为止,MIPS32/64
规范已经发布到了第6版。我们一直强调,RISC和保持指令集小没有关系。事实上,RISC的简单性,更容易让人进行扩展。
随着MIPS架构的CPU出现在嵌入式系统中,许多新的指令如雨后春笋般地冒出来。MIPS32/64吸收了一些,同时也提供了一种扩展机制ASE(Application-Specific instruction set Extensions
)。ASE作为MIPS32/64的扩展存在,可以通过配置寄存器进行选择。下面是一些选项:
MIPS32/64规范还有一些可选项,它们不能被看作为指令集的扩展:
本部分对汇编语言只做一个简单的介绍,详细的理解后面会再展开。
我们或多或少地已经接触过汇编语言,下面是MIPS架构的一小段汇编代码:
# 注释
entrypoint: # 标签
addu $1, $2, $3 # 基于寄存器的加法,等价于 $1 = $2 + $3
跟大部分的汇编语言一样,基于行的分割语言。原生注释符号是#
,编译器会忽略掉#
后面的所有文本。但是可以在一行中插入多条语句,使用;
进行分割。
标签(label)使用:
开始,可以包含各类符号。标签可以定义代码的入口点和数据存储的开始位置。
MIPS汇编程序可以使用数字标记的通用寄存器,也可以使用C语言的预处理器和一些标准头文件,这样就可以使用寄存器的别称(关于别称请参考下一节)。当然了,如果使用C预处理器,注释也可以使用C风格。
大多数指令是三目运算指令,目的寄存器在左边(与X86相反)。
subu $1, $2, $3
代表的表达式是:
$1 = $2 - $3;
目前,了解这些就足够了。
MIPS有32个通用寄存器(31),各寄存器的功能及汇编程序中使用约定如下:
下表描述32个通用寄存器的别名和用途
寄存器 | 别名 | 使用 |
---|---|---|
$0 | $zero | 常量0 |
$1 | $at | 保留给汇编器 |
$2-$3 | $v0-$v1 | 函数返回值 |
$4-$7 | $a0-$a3 | 函数调用参数 |
$8-$15 | $t0-$t7 | 临时寄存器 |
$16-$23 | $s0-$s7 | 保存寄存器 |
$24-$25 | $t8-$t9 | 临时寄存器 |
$26-$27 | $k0-$k1 | 保留给系统 |
$28 | $gp | 全局指针 |
$29 | $sp | 堆栈指针 |
$30 | $fp | 帧指针 |
$31 | $ra | 返回地址 |
实现乘法的操作有多种方式:
而MIPS架构的CPU具有一个特殊用途的整数乘法单元,独立于主流水线之外。它实现的基本操作是,将两个通用寄存器大小的值相乘,得到一个2倍于寄存器大小的结果,存储到乘法单元中。指令mfhi
和mflo
分别将结果拷贝到2个特定的通用寄存器中。
因为乘法操作执行比较慢,所以乘法单元硬件实现乘法结果寄存器互锁。后续指令如果过早读取结果的话,CPU会停止执行,直到乘法操作完成。
嵌入式编程小技巧: 能用移位实现的乘除操作,就不要使用
*
和/
运算。
整数乘法单元同样可以完成除法操作,lo
寄存器保存商,hi
寄存器保存余数。
乘法操作占用大约4-12个时钟周期,除法操作大约20-80个时钟周期(具体依赖于实现)。有些CPU还有乘法单元流水线(ARM架构就是这样实现的),也就是说,乘法操作可以在每个时钟周期都可以执行,不用再等待上一个操作完成。
MIPS32/64规范还包含一个mul
三目乘法指令,将结果的低字节保存到一个通用目的寄存器中。也就是说,这个指令只能计算相乘的结果小于寄存器大小的情况。这个指令还是执行互锁操作,也就是说等到操作完成,才能读取结果;高度优化的软件,仍然会使用分立的指令分别执行乘法操作和读取乘法结果。有些基于MIPS32/64规范的CPU还有累乘操作,连续乘法操作的结果会被相加后保存到lo/hi
寄存器中。
乘除操作从不会产生异常:即使除零操作(但是结果是不可预料的)。编译器通常产生额外的指令检查错误并捕捉错误,比如说除零操作。
指令mthi
和mtlo
,用来拷贝通用目的寄存器的值到内部寄存器中。这对于异常返回时,恢复hi
和lo
的值是必不可少的,除此之外,可能很少使用。
MIPS架构的CPU寻址方式只有一种:寄存器索引寻址。任何load和store指令都可以写成下面这样:
lw $1, offset($2)
可以使用任何寄存器作为目的或源寄存器。offset
是一个有符号的16位数(所以,范围是−32768~32768);要加载的地址是寄存器$2+offset
的值。offset
可用于索引结构体成员,数组成员或者函数栈上的变量;再或者配合gp寄存器访问全局静态变量(static和extern)。
汇编器提供了一种直接寻址的写法,但是在编译时,会将其转换成上面的机器指令格式。
更复杂的双寄存器寻址或者可变址索引寻址都必须使用多条指令才能实现。也就是说,我们在编写或者看到的汇编代码中,复杂的寻址指令都是编译器提供的伪指令,在编译阶段,编译器会将其转换成真正的机器指令。
MIPS架构CPU单条指令可以可以存取1-8个字节。
字节(byte)和半字(halfword)在load时,分为两种情况。带符号扩展指令lb和lh,将值加载到32位寄存器的低有效位,用符号位(字节的话是bit7,半字的话是bit15)填充高有效位。
数据类型 | 字节数 | 助记符 |
---|---|---|
dword | 8 | ld |
word | 4 | lw |
halfword | 2 | lh |
byte | 1 | lb |
无符号指令lbu和lhu实施0扩展;也就是说,将具体的值加载到32位寄存器的低有效位,将高有效位填充0。
比如:在地址t1处存储着值0xFE(可以解释为-2或者254(无符号)),分别使用有符号指令和无符号指令进行读取:
lb t2, 0(t1)
lbu t3, 0(t1)
那么加载完成后,t2=0xFFFFFFFFE(一个32位的有符号数-2),t3=0x000000FE(252)。
上面是按照32位描述的,对于64位也是适用的,只是操作位数扩大一倍而已。
上述短整数向长整数扩展的细微差异是C语言移植的历史原因造成的,现代C标准有明确的的规则消除可能的歧义。像MIPS这类的机器,不能直接执行8位或16位算术运算,如果涉及到short或char型变量的表达式,就要求编译器插入额外的指令保证运算正确;这应该尽量避免。当你移植代码到MIPS架构的CPU上,涉及到小整数时,要充分考虑哪些变量可以使用int型。
MIPS架构的load和store操作必须是对齐的,halfword加载以2字节为边界,32位以4字节为边界。load指令如果访问非对齐地址会产生自陷(trap)。因为CISC指令集架构比如X86架构确实能够处理非对齐load和store,所以,当你移植这上面的软件到MIPS架构上时,可能会遇到问题。也许,你会说,我可以写一个trap处理程序,在其中,模拟非对齐load操作;从而对应用程序隐藏这个硬件细节。除非,非对齐的访问比较少,否则,性能会比较差。
有时候,可能确实需要访问非对齐的数据。MIPS架构确实也提供了一个ulw
宏指令,由两个指令组成,比一个个字节的加载,移位,再相加,更高效。还有一个宏指令ulh
,使用2个load,一个移位和一个位或操作组合而成,提供非对齐的半字加载操作。
默认,C编译器会正确对齐所有数据,但是也有例外情况(比如,从文件中导入数据或者与其它CPU共享数据时),这时候可能要求能够有效地处理非对齐的整数。所以,有些编译器允许指定数据的类型为非对齐的,从而产生特殊的代码来处理。
从内存中加载浮点数到浮点寄存器中,没有任何限制。对于32位处理器,允许加载单精度值到偶数编号的浮点寄存器中。但是,你也能够使用宏指令l.d
加载双精度值。如下所示:
l.d $f2, 24(t1)
编译器会展开为两条指令:
lwc1 $f2, 24(t1)
lwc1 $f3, 28(t1)
在64位机器上,l.d
是ldc1
机器指令的优选别名。
遵循MIPS/SGI
规则的任何C编译器都会将double型浮点数按照8字节对齐。32位处理器没有这个对齐要求,但还是这样做是向后兼容:如果加载一个非8字节对齐的地址处的内容,64位CPU会陷入自陷。
前边我们或多或少提及了一些编译器的伪指令等概念,也可以成为合成指令。因为它是编译器通过多条指令合成的一个伪指令。
为什么需要伪指令呢?
因为MIPS架构只有一种寻址方式。如果我想加载一个立即数到寄存器中,需要先把立即数的地址拷贝到寄存器中,然后再使用load指令从相应的地址处加载立即数,需要两条指令。本身,汇编程序就够晦涩了,现在我只想加载个立即数,还要让我记住两条指令,这太不人道了。所以,伟大的GNU工程中的汇编器提供了合成指令。还是加载立即数,现在,我只需要使用li
(等于load immediate
)合成指令就可以写了。合成指令的命名是不是也很直接。最后由编译器生成两条机器指令。
此处,又再一次体现了MIPS架构的设计理念:硬件尽量简单,辅以软件实现。编译器提供的辅助有:
load t0, lo_addr(t1)
,在这儿t1
是临时寄存器,存放地址的高字节hi_addr
)。当然,这不适用于C函数中定义的变量,因为它们要么是在寄存器中,要么在堆栈上。-G 0
则代表关闭优化。.set noreorder
进行指定;允许的话,就是.set reorder
。如果想要查看汇编机器代码,可以借助反汇编工具objdump。
MIPS架构具有两种特权模式,用户模式和内核模式。现在,我们讨论MIPS架构对内存空间的分配使用情况。
下图是32位架构下的内存布局:
从上图可以看出,将内存空间分为了4部分:
kseg2
和kseg3
。kseg2
就保留给管理模式使用,如果使用了管理模式的话。对于非常简单的系统,大部分时候物理内存不会超过512MB。所以只需使用kseg0
和kseg1
的地址空间即可。但是,如果实在需要,可以将转换项存放于内存管理单元的TLB中,从而访问更高地址的内存。另外,如果是64位CPU,还可以使用额外的空间访问。
在内核特权下(CPU启动)可以做任何事情。在用户模式,访问高于2GB以上的地址是非法的,会产生自陷(trap)。如果CPU有MMU,意味着,用户模式下的地址必须经过MMU的转译才能访问物理内存,这样可以阻止用户模式下的程序非法访问内核模式的地址空间。这也意味着,如果MIPS架构的CPU上运行的是一个没有内存映射的OS内核,则用户特权级是多余的。
另外,在用户模式下,一些指令,尤其是OS需要的CPU控制指令是非法的。
改变内核/用户特权模式,不会改变任何行为,只是意味着某些功能在用户模式被禁止。在内核态,CPU能够访问低地址空间,就像它们处于用户模式一样,也使用相同的方式进行转换。
另外还需要注意的是,虽然看上去内核模式专门为操作系统设计的;用户模式处理日常的工作。然而,事实并非如此。很多简单的系统(包括许多实时操作系统)一直处于内核模式运行。
MIPS架构的地址总是通过一个寄存器的值加上16位的偏移计算得到。而在64位MIPS架构CPU中,寄存器的位数是64位,所以可以访问的地址空间是2^64,这样巨大的地址空间可以任由我们分配,如下图所示。
在上图中,我们可以看出,64位内存地址的扩展部分都位于32位内存地址的中间,这是一个很奇怪的实现技巧。我们知道,MIPS架构在短整数向长整数扩展时,使用了带符号位的扩展方式。在64位CPU上模拟32位指令集时,寄存器的低32位保存实际的地址值,高32位根据bit31位作为符号位进行扩展,这样32位的程序实际访问的是64位程序空间的最低2GB和最高2GB程序空间。这样,扩展的内存映射把最低空间和最高空间用作和32位系统一样的地址空间,扩展的空间就位于这中间了。
事实上,这么大的地址空间大部分时候根本没有意义,除非你正在实现一个虚拟内存操作系统,要不然基本用不上;因此,许多MIPS64用户还是把指针定义为32位长度。这些未映射的地址空间可以用来突破kseg0
和kseg1
的512MB的限制,但是,这完全可以通过内存管理单元(TLB)实现。
关于流水线的可见性,在之前的文章中已经涉及过,比如分支延迟和load延迟。任何一个带有流水线的CPU,如果有指令不能满足一个时钟周期执行完的要求的话,都会面临时序延迟的问题。如果架构设计者隐藏这些时序延迟问题,那么编程模型相对于编程人员就会变得相对容易,但是硬件实现就会复杂。而如果把时序延迟问题暴露给编程人员,让他们通过软件规避这些问题,硬件实现容易了,但是软件设计就会变得复杂。所以,这是一个平衡和选择的问题。
我们知道,MIPS架构的设计理念是:硬件尽量简单,辅以软件实现。所以,MIPS架构把一些流水线的时序延迟问题暴露给编程人员或者编译器去优化实现。下面,我们总结一下这些时序延迟问题:
rmb()
。本文分享自 嵌入式ARM和Linux 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有