计算机语言分为三类:高级语言、汇编语言和机器语言。
机器语言就是0和1的数字编码,是机器能读懂的东西,用一定长度(32或64位)的二进制数代表一条指令。
举个例子,如果我想让机器执行一条这样的指令:将1号(寄存器编号的二进制形式为 0000 1 2 00001_2 000012)寄存器的值和2号(寄存器编号的二进制形式为 0001 0 2 00010_2 000102)寄存器的值相加,运算结果存入3号(寄存器编号的二进制形式为 0001 1 2 00011_2 000112)寄存器。(寄存器:CPU内部有一定数目的寄存器(一般为32个),用来存储正在操作的数值)
那么这条机器码就是: 00000000001000100001100000100000。
这条指令的编码规则如下:
这条加法指令的结构如下所示:
规则 | OpCode | rs | rt | rd | shamat | Function |
---|---|---|---|---|---|---|
编码 | 000000 | 00001 | 00010 | 00011 | 00000 | 100000 |
含义 | R指令 | 1号寄存器 | 2号寄存器 | 3号寄存器 | 0 | 加法 |
其他指令也是按照类似的规则进行编码的。
显然人类直接用机器语言来写程序是很困难的,于是人类发明了汇编语言。
汇编语言其实是机器码的简明记录形式。
在上述例子中我们可以发现按照这样的规则进行机器编码的好处:对于加法指令而言,OpCode、shamat、Function这几部分都是一样的,有区别的仅仅是寄存器的编号,所以,这条指令记录指令的运算类型和三个寄存器编号即可。
在汇编语言中,我们将上述例子中的编码记录成如下形式:add $3, $1, $2
。
同理,减法:sub $3, $1, $2
,或:or $3, $1, $2
,异或:xor $3, $1, $2
……
虽然变得简单直观了,但是用汇编语言写程序还是很反人类的,于是人们又发明了高级语言例如C、python等,就是程序猿经常写的东西,语法上比较接近人类的自然语言。这些高级语言是汇编语言的进一步整合与简化。
举一个C语言与汇编语言转化的例子
C 语言:
if(x = 1)
x = x + 1;
else
x = 0;
汇编语言(mips-c)
#假设address_x是内存中储存变量x的位置的地址
addi $t1, $0, 1 # $t1 = 0 + 1 = 1
lw $t2, address_x #从内存中取出x的值,放入寄存器t2中
bne $t1, $t2, label_if #如果$t1与$t2不相等,跳到标签label_if处,否则继续执行
label_else:
addi $t2, $0, 0 # x = 0
j label_if_end #跳转到标签label_if_end处
label_if:
addi $t2, $t2, 1 #x = x + 1
label_if_end:
sw $t2, address_x #将x的值放回到内存中
现在,我们正式开始了解mips-c指令集。mips-c指令根据编码形式的不同分为三类:R类、I类、J类。每一条指令都可以转化成一个32位的二进制数。
R类指令有add(加法)、sub(减法)、or(或)、xor(异或)、slt(小于置1)、srl(无符号右移)、sra(符号右移)、sll(左移)等。一般表示为如下形式:
OpCode | rs | rt | rd | shamat | Function |
---|---|---|---|---|---|
6位 | 5位 | 5位 | 5位 | 5位 | 6位 |
I类指令有addi、subi、lui、lw、sw、beq等。一般表示为如下形式:
OpCode | rs | rt | immediate |
---|---|---|---|
6位 | 5位 | 5位 | 16位 |
举个例子:addi $1, $2, 100,即 $1 = $2 + 100,其机器码为:
OpCode | rs | rt | immediate |
---|---|---|---|
001000 | 00001 | 00010 | 0000 0000 0110 0100 |
I类指令有j、jr、jal、jalr。
j、jal表示为如下形式:
OpCode | address |
---|---|
6位 | 26位 |
jr、jalr指令表示为如下形式:
OpCode | rs | 0 |
---|---|---|
6位 | 5位 | 26位 |
如果能够理解以下六个指令,那么就能基本理解Mips-C指令的编码思维。
加法指令
OpCode | rs | rt | rd | shamat | Function |
---|---|---|---|---|---|
6位 | 5位 | 5位 | 5位 | 5位 | 6位 |
操作:$rd = $rs + $rt
置高位指令,将某寄存器的值的高16为置为指定立即数,低16位置为0。
OpCode | 0 | rt | immediate |
---|---|---|---|
6位 | 5位 | 5位 | 16位 |
操作:$rt = {immediate, 0000_0000_0000_0000}
例如:lui $t1, 7
运算后,$1的值为111_0000_0000_0000_0000 2 _{2} 2
store word 指令,向内存存入数据的指令。
OpCode | rs | rt | immediate |
---|---|---|---|
6位 | 5位 | 5位 | 16位 |
操作:
address = $rs + immediate
memory[address] = $rt
例如:sw $t0, 100( $t1 )
运算后,将 $rt 的值存入内存地址为 $rs + immediate 的位置。
load word 指令,从内存取出数据的指令。
OpCode | rs | rt | immediate |
---|---|---|---|
6位 | 5位 | 5位 | 16位 |
操作:
address = $rs + immediate
$rt = memory[address]
例如:lw $t0, 100( $t1)
运算后,将 内存地址为 $rs + immediate 的位置的值存入 $rt 。
分支指令,如果分支条件中两数相等,程序跳转到指定位置。
OpCode | rs | rt | immediate |
---|---|---|---|
6位 | 5位 | 5位 | 16位 |
操作:
if($rs == $rt)
PC = PC + 4 + sign_ext({immediate, 00 2 _{2} 2 })
例如:beq $t1, $t0, label
结果:如果$t1 == $t2,程序跳转到label处
跳转指令,程序跳转到指定位置。
OpCode | address |
---|---|
6位 | 26位 |
操作:
PC = {PC 31 … 28 _{31…28} 31…28,address,00}
例如:j label
结果:程序跳转到label处
syscall指令是以类特殊的指令,当 $ v0、$ a0 的值不同时,执行不同的操作。例如:$ v0 = 10时,执行syscall,程序结束,相当于C中的return 0;$ v0 = 5时,执行syscall,读入一个整数,存入$ a0中。
syscall对照表如下:
哈佛体系计算机中代码与数据分开存储。程序计数器记录程序运行到了哪一条指令,CPU运行时,读取程序计数器的值,根据指令地址,找到对应指令,然后识别指令,对寄存器或内存进行操作。
运算类的操作对象是内存和CPU内的32个寄存器。CPU完成的运算可以简单看成:从内存中取出所需的数据,暂时存到寄存器中,做运算,再将寄存器中的值放回内存中。
分支、跳转类的操作对象是程序计数器。程序计数器记录程序运行到了哪一条指令,分支、跳转类指令决定程序计数器的值如何改变。
你需要了解一些基本语句的功能,例如add、sub、or、lui、beq、lw、sw、j、jal、jr等。详情请参见MIPS—C指令集。这里有一个31条的版本:MIPS 指令集(共31条)。这边版本缺少关键指令jr、jalr,在此解释。
jr $ra #跳转到ra寄存器中的数所代表的位置
jalr $ra #跳转到ra寄存器,并将当前程序地址存入ra中
程序分为数据段(以.data标识)和代码段(以.text标识)两个部分。
.data # 在.data以下声明数据
.text # 在.text以下写代码
下面是一个例子:
.data # 在.data以下声明数据
matrix: .space 36 # 申请一个长度为9的数组,一个数4字节,所以空间大小为9 * 4字节
enter: .asciiz "\n" #存储换行符号
space: .asciiz " " #存储空格符号
.text # 在.text以下写代码
main:
la $s1, space # int *p = matrix
add $t0, $0, $0 # int i = 0;
在代码段,除了指令,你还可以看到类似于main:
、loop:
之类的标签,这些标签主要是用于方便记录跳转指令(j、jal等)、分支指令(beq、bnq等)的跳转位置。
在下面这个程序中,其实仅仅执行了指令二,而没有执行指令一,因为j main
这一行代表直接跳转到main:
标签这一行,中间的部分被略过了。
j main
add $t0, $0, $0 # 指令一
main:
add $t1, $0, $0 # 指令二
标签方便我们构造条件语句和循环语句,思考一下该如何构造?
我们可以借助宏定义完成一些函数,格式为:
.macro 函数名(参数)
.end_macro
如果我们想定义一个scanf
函数,可以写成这样:
.macro scanf(%x) #宏定义,定义一个scanf函数,传递参数为%x,%x是一个寄存器变量
li $v0, 5 #li指令为赋立即数指令,$v0 = 1
syscall #syscall,当$v0为5时,syscall执行结果为:读入一个整数,存入$v0中
move %x, $v0 #move指令,将$v0中的值移动到%x寄存器中
.end_macro
so sad ! 不同于高级语言,这里没有缩进和花括号,程序只是一条一条的执行,条件语句、循环语句、函数跳转都将由标签和宏定义来完成。
我们通过实例来感受一些汇编语言是如何编写的。
要求:读入一个整数n,再读入n个数,存入内存,再依次输出。
输入:
9
1 2 3 4 5 6 7 8 9
输出:
1 2 3 4 5 6 7 8 9
int matrix[9];
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i++)
scanf("%d", &matrix[i]);
for(int i = 0; i < n; i++)
printf("%d ", matrix[i]);
}
.data #数据空间段
matrix: .space 36 # 申请一个长度为9的数组,一个数4字节,所以空间大小为9 * 4字节
enter: .asciiz "\n" #存储换行符号
space: .asciiz " " #存储空格符号
.text #代码段
.macro scanf(%x) #宏定义,定义一个scanf函数,传递参数为%x,%x是一个寄存器变量
li $v0, 5 #li指令为赋立即数指令,$v0 = 1
syscall #syscall,当$v0为5时,syscall执行结果为:读入一个整数,存入$v0中
move %x, $v0 #move指令,将$v0中的值移动到%x寄存器中
.end_macro
.macro printf(%x)
move $a0, %x #move指令,将%x中的值移动到$a0中
li $v0, 1
syscall #syscall,当$v0为1时,syscall执行结果为,输出$a0中的整数
.end_macro
.macro printfspace()
la $a0, enter
li $v0, 4
syscall #syscall,当$v0为1时,syscall执行结果为,输出$a0中的整数
.end_macro
main:
scanf($s0) #scanf("%d", &n);
la $s1, space # int *p = matrix
add $t0, $0, $0 # int i = 0;
loop_in:
beq $t0, $s0, end_loop_in # if(i == n) break;
scanf($t1) #scanf("%d", &x);
mul $t2, $t0, 4
sw $t1, matrix($t2)
add $t0, $t0, 1 # i = i + 1;
j loop_in
end_loop_in:
add $t0, $0, $0 # int i = 0;
loop_out:
beq $t0, $s0, end_loop_out # if(i == n) break;
mul $t2, $t0, 4
lw $t1, matrix($t2)
printf($t1)
printfspace()
add $t0, $t0, 1
j loop_out
end_loop_out:
.data
fibs: .space 48
size: .word 12
space: .asciiz " "
head: .asciiz "The Fibonacci numbers are: \n"
.text
la $t0, fibs
la $t5, size
lw $t5, 0($t5)
li $t2, 1
sw $t2, 0($t0)
sw $t2, 4($t0)
addi $t1, $t5, -2
loop:
lw $t3, 0($t0)
lw $t4, 4($t0)
add $t2, $t3, $t4
sw $t2, 8($t0)
addi $t0, $t0, 4
addi $t1, $t1, -1
bgtz $t1, loop
la $a0, fibs
add $a1, $zero, $t5
jal print
li $v0, 10
syscall
print:
add $t0, $zero, $a0
add $t1, $zero, $a1
la $a0, head
li $v0, 4
syscall
out:
lw $a0, 0($t0)
li $v0, 1
syscall
la $a0, space
li $v0, 4
syscall
addi $t0, $t0, 4
addi $t1, $t1, -1
bgtz $t1, out
jr $ra #return
.data
.text
.macro scanf(%x)
li $v0, 5
syscall
move %x, $v0
.end_macro
.macro printf(%x)
move $a0, %x
li $v0, 1
syscall
.end_macro
main:
scanf($s0)
li $t0, 0
li $t1, 1
jal fib
printf($t1)
li $v0, 10
syscall
fib:
bgt $s0, 1, next
jr $ra
next:
addi $sp, $sp, -4
sw $ra, 0($sp)
addi $s0, $s0, -1
jal fib
add $t2, $t0, $t1
add $t0, $0, $t1
add $t1, $0, $t2
return:
lw $ra, 0($sp)
addi $sp, $sp, 4
jr $ra