乱码三千 – 分享实用IT技术

乱码三千 – 码出一个新世界


  • 首页

  • 归档

  • 搜索

ARM原生汇编与ARM GNU汇编的区分

发表于 2021-01-05

一、ARM汇编开发的两种的方式

ARM汇编开发指用ARM提供的汇编指令,进行ARM程序的开发。

ARM汇编开发,有两种开发方式,一种是使用ARM汇编,一种是使用ARM GNU汇编。两种汇编开发,使用的汇编指令是完全一样的,区别是宏指令,伪指令,伪操作不一样。其实两种开发方式的区别在于所使用的编译工具不一样。

对于ARM汇编,使用的是ARM公司开发的编译器,而ARM GNU汇编,是使用GNU为ARM指令集开发的编译器,也就是arm-gcc。

二、ARM的编译开发环境

两种常用的ARM的编译开发环境

  • DS5:ARM提供的集成开发软件。使用的是ARM提供的工具链进行程序编译
  • GNU开发环境: 由GNU的汇编器as,交叉编译器gcc,和链接器ld等组成

三、伪操作,宏指令,伪指令

伪操作:ARM汇编语言程序里的一些特殊指令助记符,其作用主要是完成汇编程序做各种准备工作,在源程序进行汇编时由汇编程序处理,而不是在计算机运行期间由机器执行。如程序段的定义,就属于伪操作。

宏指令:一段独立的程序代码,可插在源程序中,通过伪操作来定义。

伪指令:ARM汇编语言程序里的一些特殊指令助记符,不在处理器运行期间执行,在汇编时,被合适的ARM的机器指令代替,从而实现真正的指令操作。

四、ARM原生汇编伪操作

伪操作 语法格式 作用
GBLA GBLA Varible 声明一个全局的算术变量,并将其初始化为0
GBLL GBLL Varible 声明一个全局的逻辑变量,并将其初始化成{FALSE}
GBLS GBLS Varible 声明一个全局的字符串变量,并将其初始化成空串
LCLA LCLA Varible 声明一个局部的算术变量,并将其初始化为0
LCLL LCLL Varible 声明一个局部的逻辑变量,并将其初始化成{FALSE}
LCLS LCLS Varible 声明一个局部的字符串变量,并将其初始化成空串
SETA SETA Varible expr 给一个全局或局部算术变量赋值
SETL SETL Varible expr 给一个全局或局部逻辑变量赋值
SETS SETS Varible expr 给一个全局或局部字符串变量赋值
RLIST name LIST {list of registers} 为一个通用寄存器列表定义名称
CN name CN expr 为一个协处理器的寄存器定义名称
CP name CP expr 为一个协处理器定义名称
DN/SN name DN/SN expr DN/SN为一个双精度/单精度的VFP寄存器定义名称
FN name FN expr 为一个FPA浮点寄存器定义名称
LTORG LTONG 声明一个数据缓冲池(文字池)的开始
MAP MAP expr {, base-register} 定义一个结构化的内存表(storage map)的首地址
FIELD {label} FIELF expr 定义一个结构化内存表中的数据域
SPACE {label} SPACE expr 分配一块连续内存单元,并用0初始化
DCB {label} DCB expr {,expr}.. 分配一块字节内存单元,并用expr初始化
DCD/ DCDU {label} DCD/DCDU expr {,expr}… 分配一块字内存单元, 并用expr初始化
DCDO {label} DCDO expr {,expr}… 分配一块字对齐的字内存单元, 并用expr初始化
DCFD/DCFDU {label} DCFD{U} fpliteral ,{,fpliteral}… 为双精度的浮点数分配字对齐的内存单元
DCFS/DCFSU {label} DCFS{U} fpliteral,{,fpliteral}… 为单精度的浮点数分配字对齐的内存单元
DCI {label} DCI expr, {expr}… ARM代码分配一段字对齐的内存单元,填充expr(二进制指令码),THUMB代码中,分配一段半字对齐的半字内存单元。
DCQ/ DCQU {label} DCQ{U} {-} literal, {, {-} literal}… 分配一段以双字(8个字节)为单位的内存
DCW/DCWU {label} DCW{U} {-} literal, {, {-} literal}… DCW用于分配一段半字对齐的半字内存单元

1、AREA

创建一段新的程序代码或数据区。

格式 :

1
AREA  name, {,attr,} …

其中,name是程序段名, atrr是段名属性

对于属性,有以下一些:

  • CODE: 用于定义代码段,默认为是READONLY
  • DATA: 用于定于数据段,默认为READWRITE
  • READONLY: 指定本段的内容只读
  • READWRITE: 指定本段的内容可读可写
  • ALIGN: 指定对齐为2次幂
  • COMMON: 定义通用的段。不包含任何用户的代码和数据。各源文件中同名的COMMON属性段共享同一段存储单元

2、ALIGN

指定对齐

ALIGN 4 表示4字节地址对齐

ALIGN 8 表示8字节地址对齐

注意:在AREA中使用和单独使用ALIGN的区别,在于格式和对齐的计算不一样。

3、ENTRY

指定汇编程序的入口。

一个程序至少有一个入口点,也可以有多个入口点,但是在一个源文件中,最多只能有一个ENTRY。当多个源文件均有ENTRY时,由链接器指定程序真正的入口。

4、END

表示源程序的结束

所以汇编语言源文件必须以END结束,汇编器遇到END, 将结束编译。

5、EXPORT

格式: EXPORT 标号 [,WEAK]

声明一个全局标号,其他源文件可以使用这个标号。WEAK表示碰上其他同名标号时,其他标号优先。

6、IMPORT

格式: IMPORT 标号,[,WEAK]

表示该引用的标号在其他源文件中,单要在当前文件中引用。WEAK表示找不到该标号时,也不报错,一般该标号置为0,如果是B 或BL指令用到该标号,该指令置为nop。

该标号会加入到当前源文件的符号表中。

7、EXTERN

和IMPORT一样,不同在于,如果当前文件没有引用该标号,该标号不会加入到当前源文件的符号表中。

8、GET(或INCLUDE)

将一个源文件含到当前的源文件中

9、EQU

对一个常量标号赋值

格式:

1
name  EQU  expression

其中: name符号名, expression寄存器相关或者程序相关的固定值

如:

1
num  EQU  2  ;  为符号赋予数字2

EQU,等同于C语言中用#define定义一个常量

10、SPCAE

用于分配一片连续内存单元,并用0初始化。SPACE可用%代替。

格式:

1
{label} SPACE expr

label : 是一个标号, 可选

expr: 分配的内存字节数

如

1
stack SPACE 100 ; 分配100个字节内存单元,并用0初始化。标号stack是这片空间的起始地址

11、DCB

用于分配段字节内存单元,并用伪操作中的expr初始化。

格式:

1
{label} DCB expr {,expr}

label: 是一个标号,可选

expr: 可以是-128~255的数值或者字符串

如:

1
string  DCB  "HELLO"  ;为HELLO字符串分配空间, string是这块空间的起始地址

12、DCD及DCDU

用于分配段字内存单元(分配的内存都是字对齐,DCDU并不严格字对齐),并用伪操作中的expr初始化。 DCD 可用 & 代替。

格式:

1
{label} DCD expr, {,expr}

label: 是一个标号,可选,表示这块内存单元的首地址

expr: 数字表达式或程序中的标号

如:

1
data DCD  1,2,3,4   ;分配字对齐的字单元空间,初始化为1,2,3,4

五、ARM原生汇编伪指令

ARM伪指令包括: ADR, ADRL,LDR ,NOP

THUMB伪指令包括:ADR, LDR, NOP

伪指令 语法格式 作用
ADR ADR{cond} register, expr 将基于PC或基于寄存器的地址值读取到寄存器中。小范围的地址读取
ADRL ADRL{cond} register, expr 将给予PC或基于寄存器的地址值读取到寄存器中。中等范围的地址读取
LDR LDR {cond} register, =[expr|label] 将一个32位的立即数或者一个地址值读取到寄存器中。大范围的地址读取
NON NOP 在汇编时,被替换成空操作

六、ARM GNU编译环境

伪操作 语法格式 作用
.byte .byte expr {,expr}… 分配一段字节内存单元,并用expr初始化
.hword/.short .hword expr {,expr}… 分配一段半字内存单元,并用expr初始化
.ascii .ascii expr {,expr}… 定义字符串expr
.asciz/.string .asciz expr {,expr}… 定义字符串expr(会增加/0为结束符)
.floar/.single .float expr {,expr}… 定义32bit IEEE浮点数expr
.double .doubel expr {,expr}… 定义64bit IEEE浮点数expr
.word/.long/.int .word expr {,expr}… 分配一段字内存单元,并用expr初始化
.fill .fill repeat {,size} {,value} 分配一段字节内存单元,用sieze长度value填充repeat次
.zero .zero size 分配一段字节内存单元,并用0填充内存
.space/.skip .space size, {,value} 分配一段内存单元,用value将内存初始化
.section .section expr 定义一个段
.text .text {subsection} 代码段,
.data .data{subsection} 数据段
.bss .bss{subsection} bss段
.cond 16/.thumb .code 16/.thumb 表示之后的汇编指令使用THUMB指令集
.code 32/.arm .code 32/.arm 表示之后的汇编指令使用ARM指令集
.end .end 标记汇编文件的结束
.include .include “filename” 将一个源文件包含到当前源文件中
.align/.balign .align {alignment} {,fill},{max} 通过填充字节使当前位置满足一定的对齐格式

七、两种开发环境的区别

两种开发环境下的汇编代码,有较多不同的点,主要是符号及伪操作的不同。

ARM汇编的伪操作符 GNU汇编的伪操作符
INLCUDE .include
NUM EQU 25 .equ NUM, 25
EXPORT .global
IMPORT .extern
DCD .long
IF: DEF: .ifdef
ELSE .else
ENDIF .endif
OR |
SHL <<
RN .req
GBLA .global
NUM SETA 16 .equ NUM , 16
MACRO .macro
MEND .endm
END .end
AREA WORD, CODE, READONLY .text
AREA BLOCK, DATE, READWRITE .data
CODE32 .arm
CODE16 .thumb
LTORG .ltorg
% .fill
ENTRY ENTRY:
ldr x0,=0xff ldr x0,=0xff

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

汇编语言之ARM32汇编

发表于 2021-01-05

ARM两种编译环境

两种常用的ARM的编译开发环境

  • ARM原生编译环境:ARM官方提供的原生编译环境,相关集成开发软件有ADS,Keil等,常用于ARM单片机开发
  • GNU编译环境: 由GNU的汇编器as,交叉编译器gcc,和链接器ld等组成,一般适用于交叉编译环境需求

以上两种编译环境,使用的指令集都是一致的, 只是语法格式有不同,也就是宏指令,伪指令,伪操作不一样

ARM原生环境搭建

使用 Keil μVision5 这款软件进行ARM32的汇编学习

下载地址:http://www.mcuzone.com/down/Software.asp?ID=10000503

ARM32系列命名

ARM产品 ARM架构
ARM7 ARM v4
ARM9 ARM v5
ARM11 ARM v6
Cortex-A ARM v7-A
Cortex-R ARM v7-R
Cortex-M ARM v7-M

寄存器

在ARM32中一共有37个寄存器,其中包含16个通用寄存器(R0~R15)和1个状态寄存器 ,15个通用影子寄存器,5个状态影子寄存器

影子寄存器

如上图所示,在ARM32中一共有7中不同的处理器模式,分别为:用户模式(User),快速中断模式(FIQ),普通中断模式(IRQ),管理模式(Svc),数据访问中止模式(Abort),未定义指令中止模式(Und),系统模式(Sys)

但是在不同的模式下,同样的一个寄存器名称指向不同的物理寄存器,这些不同的物理寄存器就被称之为影子寄存器

由于这些影子寄存器也属于通用寄存器的范畴, 因此很多人也将ARM32的通用寄存器归纳为31个

语法

  1. 注释(两种方式)

    1
    2
    3
    ; 我是注释

    /*…我是注释..*/
  2. 声明一个代码段

    1
    2
    3
    4
    5
    6
    7
    8
     	AREA test, CODE	 ;声明一个代码段,段的名称为test(名称可自定义),CODE关键字大小写都可,为了区分,一般大写

    ;=========在此编写汇编代码==========

    END ;END表示编译结束标记


    ;段的声明需要以制表符开头,前面留出空
  3. 数据表示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mov R0,#13 ;将十进制13赋值给R0寄存器

    mov R0,#0x13 ;将十六进制0x13赋值给R0寄存器


    mov R0,#8_12 ;将8进制数12赋值给R0寄存器 N进制则为#N_xxx


    mov R0,#'a' ;将字符a对应的ascii码值传送给R0寄存器
  4. 函数声明和调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
      	AREA test, CODE


    bl print ;函数调用

    print ;函数名称
    mov R3,#5
    bx lr
    END ;END表示编译结束标记
  1. 声明一个数据段

    1
    2
    AREA da, DATA	 ;声明一个数据段 默认可读可写状态
    ;数据定义伪操作
  1. 数据定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # DCB   
    Str DCB "This is a test!" ;分配一片连续的字节存储单元并初始化。取名为Str 类似于8086中的db
    Str = "This is a test!" ;DCB也可用等号代替


    #DCW
    Str DCW 1,2,3 ;定义字型数据 每个字符占用2个字节的空间


    #DCD
    Str DCD 1,2,3 ;定义半字型数据 每个字符占用4个字节的空间

    字符串必须使用DCB进行定义

  2. 分配一块连续的内存空间

    1
    2
    3
    4
    sp1 SPACE 100 ;分配一块连续100个字节的空间

    ;或者使用%代替SPACE简写
    sp1 % 100 ;分配一块连续100个字节的空间

代码编写规范

  1. 所有指令和伪指令不允许顶格
  2. 所有变量和标签必须顶格
  3. 一般我们将伪指令大写,变量和标签小写

内存数据的读写

  1. 从内存中读取数据

    使用中括号表示通过地址取值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    LDR R0,[R1]               ;将内存地址为R1的字数据读入寄存器R0。
    LDR R0,[R1,R2] ;将内存地址为R1+R2的字数据读入寄存器R0。

    LDR R0,[R1,#8] ;将内存地址为R1+8的字数据读入寄存器R0。
    LDR R0,[R1,R2]! ;将内存地址为R1+R2的字数据读入寄存器R0,并将新地 址R1+R2写入R1。



    LDR R0,[R1,#8]! ;将内存地址为R1+8的字数据读入寄存器R0,并将新地址 R1+8写入R1。

    LDR R0,[R1],R2 ;将内存地址为R1的字数据读入寄存器R0,并将新地址 R1+R2写入R1。


    LDR R0,[R1,R2,LSL#2]! ;将内存地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。


    LDR R0,[R1],R2,LSL#2 ;将内存地址为R1的字数据读入 寄存器R0,并将新地址R1+R2×4写入R1。


    ;从标号即为地址
    LDR R0,label ;将标号对应的内容赋值给R0

复杂格式如LDR R0,[R1],R2,LSL#2 其中 []运算优先
2. 向内存中写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STR R0,[R1],#8           ;将R0中的字数据写入以R1为地址的内存中,并 将新地址R1+8写入R1。


STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的内存中。

STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地 址的内存中。
STRB R0,[R1,#8] ;将寄存器R0中的字节数据写入以R1+8为地址的存 储器中。
STR R0,[R1],#8 ;将R0中的字数据写入以R1为地址的内存中,并 将新地址R1+8写入R1。


STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的内存中。

STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地 址的内存中。
STRB R0,[R1,#8] ;将寄存器R0中的字节数据写入以R1+8为地址的存 储器中。

LDR伪指令

这个指令和内存读取指令长的一模一样,如果我们在使用的时候加个等号,那么它就是另外一个指令

1
2
3
4
5
6
;如果不加等号 是内存读取功能
LDR R0,label ;获取标签所对应的内存数据赋给R0

;一旦加了等号,则变成了传送指令
LDR R0,=label ;将标号对应的实际物理地址值赋值给R0 此时它的作用和mov无异
LDR R0,='a' ;直接将字符数据传送给R0

实际上,加了等号的LDR指令,刚好可以弥补mov指令的不足, mov指令只能传送由八个二进制位右移而得的数据,而LDR则没有这个限制

也就是说如果我们想将一个数值传入寄存器,可以有两种方式:

1
2
3
4
5
6
7
8
9
10
;第一种
mov R1,#0X100

;第二种
ldr R1,=0x100

;mov指令的限制:只能传送由八个二进制位右移而得的数据, 也就相当于是两个十六进制数据,由于可以不断移位那么数据的大小可以伸缩,比如以下数据都可使用mov指令
0x00000058 0x00000580 0x00005800 0x00058000 0x00580000 0x05800000 0x58000000
;我们发现一个规律:mov指令只能传送最大两个十六进制空间的数据,注意是空间,这两个数据随意你移动,一旦不满足这个条件则无法传送,比如
0x00000581

LDR伪指令总结

作用:

  • 弥补mov指令的不足
  • 获取数据所对应的内存地址

ADR指令

那么除了通过LDR伪指令来获取数据所在地址外还有一个指令也可以获取数据地址,那就是adr指令,但这个指令只能获取当前段内数据的地址,段外数据无法获取,ldr则没有这个限制

1
2
3
4
5
	AREA test2,CODE
mov R3,#8
aaaa dcb 1,2,3
adr R0,aaaa ;获取aaaa首地址
END

段的拓展

段属性拓展

  1. 段读写属性
  • READONLY

    该段内存区域数据只能读取,不能写入,也就是如果使用内存读写指令,数据也写入不了

  • READWRITE

    该段内存区域可读可写,不仅可以使用内存读写指令,还可以在调试的时候直接在memory窗口双击修改

示例:

1
2
AREA code, CODE,READWRITE ;将代码段内存区域设置为可读可写状态,如果不写默认为只读
AREA code, DATA,READONLY ;将数据段内存区域设置为只读状态,如果不写默认为可读可写
  1. 段对齐属性 ALIGN

    1
    AREA code, DATA,ALIGN=3  ;对齐数值范围为0~31

    该属性会使得该段的基地址进行相应的偏移,ALIGN=3表示基地址会在上一个段数据的基础之上偏移2^3=8个字节的位置

    1
    2
    3
    4
    5
    6
    7
    8
       	 AREA test1,DATA  ;假设这个段的基地址为0x00000100
    STR1 = "A"

    AREA test2,DATA ;ALIGN默认为2 那么这个段的基地址为0x00000104
    STR2 = "B"

    AREA test3,DATA,ALIGN=8 ;这个段的基地址为0x0000010C
    STR2 = "B"

    我们可以简单理解为,使用ALIGN这个属性可以让我们给上一个段预留除一部分缓冲区域,以ALIGN=2为例,当上一个段中的数据超过4个字节时,当前段基地器会向后再偏移4个字节,避免数据被覆盖,也就是说内存数据位置会进行重新分布,那么我们可以通过这个值来设置内存数据刷新频率,值越低,内存利用率越高,但是内存刷新频率也越高,负荷加重,反之,内存浪费越大,但是内存数据不需要频繁重新分布

    另外:

    除了在段属性中可以设置对齐之外,在指令中也可以插入ALGIN关键字:

    1
    2
    3
    4
    5
    6
    7
    8
    AREA code,CODE	   

    mov R0,R1

    ALIGN 4,3 ;下一条指令4字节对齐,并且偏移3个字节 为了补满4个字节,用0填充剩余1个未偏移位置
    mov R2,R0

    END

代码中使用AGLIN时用空格代替等号,同时单位为字节

多个代码段入口区分

当我们在同一个源文件中定义两个代码段时,程序从哪个段当做执行入口呢?

这个时候我们需要指定程序的入口,使用伪指令entry

1
2
3
4
5
6
7
8
9
10
 AREA test2,CODE 

mov R0,#7

AREA test3,CODE
entry ;程序入口
mov R0,#6
mov R1,#0x00000100
str R0,[R1]
END

栈的操作

  1. 入栈
1
2
3
4
5
push {R0}  ;将R0中的值存入栈内存中  相当于是STR R0,[R13,#-4]

入栈过程:
* 第一步:将栈顶指针往低地址偏移四个字节
* 第二步:将数据存入指针指向的内存空间
  1. 出栈
1
2
3
4
5
pop {R0}  ;将栈顶中的值取出存入R0寄存器 相当于是LDR R0,[R13],#0x0004

出栈过程:
* 第一步:将栈顶指针指向的内容取出存入寄存器
* 第二步:将指针往高地址恢复四个字节

pop和push 本质上使用的是LDR和STR内存读写指令

对栈批量操作

如果想批量操作多个连续栈空间的话,直接使用逗号分隔开,连续标号的寄存器使用横杠分隔

1
2
3
4
push {R0,R4-R12,LR} ;大括号中寄存器从右往左LR R12...R4 R0依次存入


pop {R0,R4-R12,PC} ;从左往右取出

除了使用pop和push之外,可以使用STM(store much)和LDM(load much)指令

格式:

1
STM 起始地址/基地址寄存器,{寄存器名称,多个寄存器以逗号或者-分割} ;起始地址寄存器R0-R14任意选择

示例:

1
2
3
4
5
6
7
8
9
STMFD  R13!,{R0,R4-R12,LR}    ;将寄存器列表中的寄存器(R0,R4到 R12,LR)存入栈,。
等价于
push {R0,R4-R12,LR} ;大括号中寄存器从右往左存入



LDMFD R13!,{R0,R4-R12,PC} ;将栈内容恢复到寄存器(R0,R4到R12,LR)。
等价于
pop {R0,R4-R12,PC} ;从左往右取出

pop和push,它们内部也是转成STM和LDM指令:

批量存取指令扩展

相关后缀含义:

  • IA:(Increase After):数据操作后基地址增4
  • IB:(Increase Before):数据操作前基地址增4
  • DA:(Decrease After):数据操作后基地址减4
  • DB:(Decrease Before):数据操作前基地址减4
  • FD: 满递减堆栈 (相当于STMDB+LDMIA)
  • FA: 满递增堆栈 (相当于STMIB+LDMDA)
  • ED: 空递减堆栈(相当于STMDA+LDMIB)
  • EA: 空递增堆栈 (相当于STMIA+LDMDB)

我们在使用的时候,要么使用结合的形式比如STMDB+LDMIA,要么直接使用封装形式比如STMFD+LDMFD

满栈和空栈

栈的生长方式可以有四种: 满增栈、满减栈、空增栈、空减栈

  • 满栈(full stack):栈指针指向下一个将要取出数据的位置,数据入栈时,栈顶指针先偏移再入栈,数据出栈是,先取数据,后指针偏移。
  • 空栈(empty stack):栈指针指向下一个将要放入数据的位置,数据入栈时,先存数据后指针偏移,数据出栈时,先指针偏移,后数据取出
  • 递增堆栈(ascending stack):堆栈由低地址向高地址生长。
  • 递减堆栈(secending stack):堆栈由高地址向低地址生长。

X86和mips架构都是采用满递减堆栈方式处理栈空间,ARM架构四种方式均支持

内存批量读写示例:

1
2
3
4
5
mov  R1,#4
mov R2,#5
mov R0,#0x00000008
stm R0,{R1,R2}
;以上四行代码表示 从0x00000008这个内存地址开始 将R1和R2中的数据依次存入

如果我想在上面的基础上再往后追加数据

1
2
3
mov  R1,#6
mov R2,#7
stm R0,{R1,R2}

我们发现数据并没有追加,而是被覆盖了,因为R0的值依然还是 0x00000008,这个时候我们需要使用扩展指令,如下:

1
2
3
4
5
6
7
8
mov  R1,#4
mov R2,#5
mov R0,#0x00000008
STMIA R0!,{R1,R2} ;只有在寄存器后加上!才能修改寄存器中的值

mov R1,#6
mov R2,#7
stm R0,{R1,R2}

事实上 STM指令如果不加后缀写法,默认使用的是STMIA指令,LDM指令默认使用LDMIA

多寄存器数据存放顺序

不管使用哪种扩展指令,皆为左低右高的形式,也就是左边的寄存器数据存放在低地址,右边的存放在高地址

1
2
3
4
STMIA  R0!,{R1,R2} ;左边R1的内容放低地址,右边R2的内容放高地址


LDMDB R0!,{R1,R2} ;高地址数据放入R2,低地址数据放入R1

宏

  1. 宏匹配

    • 语法格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    MACRO
    $label macroname $param
    ;指令序列
    MEND

    ;例如声明一个宏名为print的宏语句
    MACRO
    $label print $param ;带$的表示会被替换的内容
    ;这里写相关汇编代码
    MEND
    • 使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
     MACRO
    $label putR0 $param

    mov R0,$param

    MEND

    ; 使用
    putR0 #10
  • 延伸

第一个$label是干嘛用的呢,由于宏的内部处理方式的替换,为了避免标签名称的冲突,增加一个标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;假如我要在宏匹配中定义一个函数fun, 当我调用两次的时候,会出现函数名重复的问题
MACRO
$label putR0 $param
fun
mov R0,$param

MEND



putR0 #10
putR0 #10

;那么如果要解决这个问题的话,我可以利用第一个替换参数如下:
MACRO
$label putR0 $param
$label
mov R0,$param

MEND



fun1 putR0 #10 ;函数名为fun1
fun2 putR0 #10 ;函数名为fun2
  1. 宏定义

    • 全局宏的定义,可跨段访问
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;全局数字变量
    GBLA number
    number SETA 0Xaa

    ;全局逻辑变量
    GBLL flag
    flag SETL {TRUE}


    ;全局字符串变量
    GBLS str
    str SETS "hello world"
    • 局部宏的定义,只能在当前宏内被访问
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;局部数字变量
    LCLA number
    number SETA 0Xaa

    ;局部逻辑变量
    LCLL flag
    flag SETL {TRUE}


    ;局部字符串变量
    LCLS str
    str SETS "hello world"
    • 全局常量的定义
    1
    num   EQU      10  ;关键字EQU  数据不允许修改
  1. 宏定义示例

    1
    2
    3
    4
    5
    6
    7
    ;定义一个全局整型数据宏
    GBLA number
    number SETA 0Xaa

    ;使用
    mov R0,#number ;相当于mov R0,#0Xaa
    LDR R0,=number ;相当于LDR R0,=0Xaa

    局部宏数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    MACRO ;声明一个宏
    $label message $a ;取名为message 参数为$a

    ;定义宏内局部变量
    LCLA number
    number SETA $a

    mov R0,#number


    MEND ;宏结束,局部变量不再起作用


    message 10 ;直接调用

    常量数据

    1
    2
    3
    4
    5
    6
    7
    8
    	AREA data ,DATA
    num EQU 10
    AREA code ,CODE

    mov R1,#5
    ldr R1,=num

    END

宏匹配和宏定义需要遵循先定义后使用的原则

如果需要从宏中跳出,可以使用伪指令MEXIT

  1. 宏替换

    使用include或者get伪指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
       ;使用get伪指令
    AREA code, CODE
    GET pangshu.s ;通知编译器当前源文件包含源文件 softool.s
    GET C:\pp.s ;通知编译器当前源文件包含源文件 C:\cn.s
    END

    ;使用include伪指令
    AREA code, CODE
    include pangshu.s ;通知编译器当前源文件包含源文件 softool.s
    include C:\pp.s ;通知编译器当前源文件包含源文件 C:\cn.s
    END

指令学习

传送指令

  1. 正常传送指令mov

    1
    mov R0,#4 ;将4传送至R0寄存器
  2. 取反传送指令mvn,也叫数据非传送指令

    1
    mvn R0,#4 ;将4取反后传送至R0寄存器  0100取反为1011

转移指令

  1. B指令

直接跳转,仅更改PC寄存器的值

示例:

1
2
3
B 0x00040000 ;直接跳转到物理地址0x00040000读取指令并执行

B 标号 ;直接跳转到标号处
  1. BL指令

跳转并链接,除了更改PC寄存器的值之外,还会将下一条指令所对应的物理地址存放至lr寄存器中

示例:

1
2
3
4
5
6
BL 0x00040000
mov r1,3 ;假设这行指令对应物理地址为0x0040004, 那么BL一旦执行,会将该值存入lr寄存器


或者
BL 标号
  1. BX指令

    跳转并切换状态

    BX指令后面只能跟寄存器,弥补了B指令和BL指令的不足, 同时会根据寄存器中最低比特位值切换ARM/Thumb模式

示例:

1
2
3
4
5
6
BL print


print
mov r1,#1
BX lr ;函数返回 如果R0[0]=1,则进入Thumb状态 反之进入ARM模式

除了通过指令来更改PC寄存器值之外,在ARM32中还可以直接使用传送指令对PC寄存器进行赋值:

1
2
3
mov pc,#0x00000008  ;往pc寄存器中写入一个地址值

mov R0,pc ;获取pc寄存器中的值
  1. BLX指令

    该指令将以下功能集于一身

    • 更改PC寄存器的值
    • 将下一条指令的地址存入lr寄存器
    • 根据寄存器中最低比特位值切换ARM/Thumb模式

算术和逻辑运算指令

  1. 算术运算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    add R0,#5
    add R0,R1 ;加法


    sub R0,#5
    sub R0,R1 ;减法

    mul R0,R1,R2 ;乘法指令, 这里至少三个寄存器参与

    mla R0,R1,R2,R3 ;先乘后加 R0 = R1 × R2 + R3
  2. 逻辑运算

    1
    2
    3
    4
    5
    6
    7
    8
    and R0.#3
    and R0,R0 ;逻辑与

    orr R0.#3
    orr R0,R0 ;逻辑或

    eor R0.#3
    eor R0,R0 ;逻辑异或

移位指令

1
2
3
MOV   R0, R1, LSL#2    ;将R1中的内容左移两位后传送到R0中。

MOV R0, R1, LSR#2 ;将R1中的内容右移两位后传送到R0中,左端用零来填充。

比较指令

  1. 比较两个值是否相等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cmp R0,R1 
    beq sub ;如果两个寄存器中的值相等则跳转到sub函数中,否则继续往下执行

    sub


    cmp R0,#5
    bne sub ;如果两个值不相等,跳转到sub函数,否则继续往下执行

    sub
  2. 大于和小于(带符号)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ;大于
    cmp R0,R1
    bgt sub ;如果R0寄存器中的值大于R1,则跳转至sub

    sub

    ;小于
    cmp R0,R1
    blt sub ;如果R0寄存器中的值大于R1,则跳转至sub

    sub

标志寄存器

试想一下,我们的比较指令cmp,它内部是如何进行数据大小判断的

在高级语言里,直接使用>或者<运算符,来判断两个值的大小,比较结束后返回True或者Flase,可是在汇编语言里面没有这么简便,那它又是如何对两个数据之间大小进行判断的呢?

别忘了, 计算机最擅长做二进制的算术和逻辑运算

1
cmp R0,R1

要想判断两个数据是否相等,或者大于小于,直接做个减法运算不就完事了,也就是R0-R1,如果结果为0,那么两个值相等,如果结果为正数,则R0>R1,结果为负数,则小于

但是问题来了,这个结果值放在哪里呢?放内存中还是寄存器呢? 答案是:寄存器

cpu设计者为了方便区分专门用了一个寄存器来存放数据运算后的结果,这个寄存器叫做状态寄存器,也叫标志寄存器

ARM32中一个寄存器有32二进制位的数据空间,那该怎么存放呢?

1
00000000000000000000000000000000  ;32个二进制位

为了方便程序员开发,设计者给这些二进制位进行了相应的命名:

当两个比较值相等,进行减法运算时,结果为0,那么Z标志位的值为1,也就是

1
01000000000000000000000000000000  ;32个二进制位

如果不相等,结果不为0,那么Z标志位的值变成0

由于每个二进制位只能存0和1两个值,也就是最多只能表示两种状态,那大于和小于的状态表示就得放到另外一个二进制上了,由于二进制运算涉及到有符号和无符号两种情况,因此需要用到两个二进制分别进行处理,有符号的的结果存放在N标志位,无符号的结果存放在C标志位:

cmp指令会同时对两个数据进行有符号和无符号运算

有符号运算,如果结果为正数,N标志位值为0,如果为负数,N标志位值为1

无符号运算,如果结果为正数,C标志位值为1,如果为负数,C标志位值为0

那么我们在使用cmp指令的时候,到底是根据那个标志位的结果进行判断的呢?

如果我们使用bne指令,那么取Z标志位的值进行参考

如果我们使用blt,bgt,那么取N标志位,Z标志位和V标志位三者的值进行参考

总结:

  1. cmp指令的功能相当于减法指令,只是对操作数之间运算比较,结果间接保存在标志寄存器高位中

  2. bne,blt,bgt等这些指令都是通过获取标志寄存器中的值来得知比较结果从而进行相应跳转,不同的指令需要满足不同的条件

  3. 我们可以通过改变状态寄存器中的值来改变代码的走向

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AREA test,CODE

mov R0,#5
mov R1,#6
cmp R0,R1
;在跳转之前改变状态寄存器的值 使得bgt必然跳转
MSR cpsr_f ,0x20000000
BGT fun
mov R1,#6

fun
mov R0,#4
bx lr


END

知识扩展:

  1. 状态寄存器的读取和写入

    读取指令:MRS{mov to register from special register)

    写入指令: MSR(Move to Special register from Register )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 读取状态寄存器中的值
    mrs R0,cpsr ;将值读取到R0寄存器


    #修改状态寄存器中的值
    msr cpsr_c,#0x2d ;修改控制位区域
    msr cpsr_x,#0x2d00 ;修改扩展位区域
    msr cpsr_s,#0x2d0000 ;修改状态位区域
    msr cpsr_f,#0x2d000000 ;修改标志位区域

    #修改状态寄存器高位值时,低位必须补足0,虽然加了0但是不会影响其他区域的值
  2. 状态寄存器中各个区域具体描述

    一共分位四个大区域,从低到高分别为:控制位区域, 扩展位区域, 状态位区域, 标志位区域,每个区域各占8个二进制位的空间

    以下是控制位区域细分详解图:

  1. 比较指令标志位条件参考表
指令 含义 需要满足的条件
beq 相等 Z标志位为1
bne 不相等 Z标志位为0
bgt 带符号大于 Z标志位为0,且N和V标志位值相等
blt 带符号小于 N不等于V
bge 带符号大于等于 N等于V
ble 带符号小于等于 Z标志位为1或者N不等于V
bls 无符号小于等于 Z标志位为1且C标志位为0
bhi 无符号大于 Z标志位为0且C标志位为1
bcs 无符号大于等于 C标志位为1
bhs 无符号大于等于 C标志位为1
bcc 无符号小于 C标志位为0
blo 无符号小于 C标志位为0
bmi 负数 N标志位为1
bpl 正数或零 N标志位为0
bvs 溢出 V标志位为1
bvc 未溢出 V标志位为0
bnv 无条件执行 忽略
bal 无条件执行 忽略

条件和循环伪指令

IF、ELSE 和 ENDIF

  • 根据条件的成立与否决定是否执行某个程序段

  • IF、ELSE、ENDIF 伪指令可以嵌套使用

  • 1
    2
    3
    4
    5
    6
    7
    8
    GBLL Test ;声明一个全局逻辑变量Test
    Test SETL {TRUE}

    IF Test = {TRUE}
    程序段1
    ELSE
    程序段2
    ENDIF

WHILE 和 WEND

  • 根据条件的成立与否决定是否重复汇编一个程序段

  • 若 WHILE 后面的逻辑表达式为真,则重复汇编该程序段,直到逻辑表达式为假

  • WHILE 和 WEND 伪指令可以嵌套使用

  • 1
    2
    3
    4
    5
    6
    GBLA Counter  ;声明一个全局数字变量Counter
    Counter SETA 3 ;赋值
    ...
    WHILE Counter < 10
    程序段
    WEND

汇编语言和C语言交互

  • 内嵌汇编
  • 外链汇编

1.引入其他源文件函数

使用import或者extern伪指令

1
2
3
4
5
6
7
8
9
;使用import伪指令
AREA code, CODE
import fun1 ;导入其他源文件中名为fun1的函数
END

;使用extern伪指令
AREA code, CODE
extern fun1
END

两者区别:

  • import:不管当前文件是否使用该引入的函数,该标签都会加入当前文件符号表,即为静态引用
  • extern:只有当前文件使用了该函数,才会将此标签加入符号表,即为动态引用

2.导出当前源文件中函数供其他文件访问

使用export或者global伪指令

1
2
3
4
5
6
7
8
;使用import伪指令
AREA code, CODE
export fun ;导出fun函数供其他源文件使用

fun
mov R0,#4
bx lr
END

3.外链汇编之C语言调汇编函数

第一步,在汇编原文件中将函数暴露出来给供外部调用,使用export或者global伪指令:

1
2
3
4
5
6
7
8
9
10
11
12
AREA code, CODE
export arm_strcpy ;或者使用global

arm_strcpy
loop
ldrb R4,[R0],#1 ;如果使用ldr 那么将偏移值改成4
cmp R4,#0
beq over
strb R4,[R1],#1
b loop
over
END

第二步,在C文件中引用汇编中的函数,C文件中只能使用extern伪指令:

1
2
3
4
5
6
7
extern arm_strcpy(char *src,char*des);

int main2(){
char *a="hello pangshu";
char b[64];
arm_strcpy(a,b);
}

4.外链汇编之汇编调c语言函数

第一步,在C文件中编写好函数

1
2
3
int c_sum(int a,int b){
return a+b;
}

第二步, 在汇编文件中引入函数,使用import或者extern伪指令

1
2
3
4
5
6
7
AREA code, CODE
import c_sum

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数

END

第三步, 使用BL指令调用函数

1
2
3
4
5
6
7
AREA code, CODE
import c_sum

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数
BL c_sum
END

在ARM中函数参数使用R0~R3这四个寄存器来进行传递,最多传递4个参数,超过4个参数使用栈进行处理,函数返回值通过R0进行传递

由于keil软件的特殊性,我们可以通过以下方式进行互调测试

C文件中代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
extern arm_strcpy(char *src,char*des);

int main2(){

char *a="hello pangshu" ;

char b[64];

arm_strcpy(a,b); //调汇编中函数
return 0;
}

int c_sum(int a,int b){
return a+b;
}

汇编文件中代码:

1
2
3
4
5
6
7
8
9
10
11
 AREA code, CODE

import c_sum
export arm_strcpy

arm_strcpy

mov R0,#1 ;第一个参数
mov R1,#2 ;第二个参数
BL c_sum ;结果存放至R0中
END

5.内嵌汇编

在C语言中嵌入汇编代码,格式如下:

1
2
3
4
5
6
7
8
9
10
11
int main2(){
int a=4;
int b=4;
int c=4;

__asm__{ //使用__asm或者__asm__
mov R5,#0x00000005 //在大括号内部直接写入汇编代码即可
mov R6,#0x00000005
}
return 0;
}

内嵌汇编的注意事项:

  • 不能直接给PC寄存器赋值,如果想改变pc值需要借助转移指令
  • 由于R0R3用于存放函数参数和返回值,R12R15有特殊用途,因此我们能操作的寄存器只有R4~R11, 又因为编译器会优先将寄存器分配给函数中的局部变量,因此我们一般无法在内嵌汇编环境中准确地修改某个寄存器的值,比如我想修改R5寄存器的值,由于函数有个变量占用了R5这个寄存器,那么编译器会自动将你写的这个R5改成R6或者其他,所以,在内嵌汇编时我们需要把寄存器当作变量来看待,把局部变量也当成寄存器看待,就好理解了
1
2
3
4
5
6
7
8
9
10
11
void c_strcopy(char *src,char *des){
char ch
__asm__{
loop:
ldrb ch,[src],#1 //局部变量当成寄存器看待
strb ch,[des],#1
cmp,ch,#0
bne loop
}

}

ARM32中寄存器别名补充

寄存器 别名 用途
r0 a1 第一个函数参数和函数返回值
r1 a2 第二个函数参数
r2 a3 第三个函数参数
r3 a4 第四个函数参数
r4 v1 寄存器变量
r5 v2 寄存器变量
r6 v3 寄存器变量
r7 v4 寄存器变量
r8 v5 寄存器变量
r9 v6 寄存器变量 实际的帧指针
r10 sl 栈接线
r11 fp 参数指针
r12 ip 临时
r13 sp 栈指针
r14 lr 连接寄存器
r15 pc 程序计数器

如何编译16位arm汇编指令

1
2
3
4
AREA test, CODE
code16 ;声明为16位arm指令 如果不写默认则为code32

END

附:指令集汇总

(一) ARM 指令集

1. 指令格式

2. 条件码

3. ARM 存储器访问指令

1) LDR/ STR -加载 /存储指令

2) LDM/ STM -多寄存器加载 /存储指令

3) SWP -寄存器和存储器交换指令

4. ARM 数据处理指令

1) 数据传送指令

a) MOV -数据传送指令

b) MVN -数据非传送指令

2) 算术逻辑运算指令

a) ADD -加法运算指令

b) SUB -减法运算指令

c) RSB- 逆向减法指令

d) ADC -带进位加法指令

e) SBC -带进位减法指令

f) RSC -带进位逆向减法指令

g) AND -逻辑“与”

h) ORR -逻辑“或”

i) EOR -逻辑“异或”

j) BIC -位清除指令

3) 比较指令

a) CMP -比较指令

b) CMN -负数比较指令

c) TST -位测试指令

d) TEQ -相等测试指令

4) 乘法指令

a) MUL - 32位乘法指令

b) MLA - 32位乘加指令

c) UMULL - 64位无符号乘法指令

d) UMLAL - 64位无符号乘加指令

e) SMULL - 64位有符号乘法指令

f) SMLAL - 64位有符号乘加指令

5. ARM 分支指令

1) B -分支指令

2) BL -带连接的分支指令

3) BX -带状态切换的分支指令

6. ARM 协处理器指令

1) CDP -协处理器数据操作指令

2) LDC -协处理器数据读取指令

3) STC -协处理器数据写入指令

4) MCR - ARM处理器到协处理器的数据传送指令

5) MRC -协处理器到 ARM处理器的数据传送指令

7. ARM 杂项指令

1) SWI -软中断指令

2) MRS -读状态寄存器指令

3) MSR -写状态寄存器指令

8. ARM 伪指令

1) ADR -小范围的地址读取伪指令

2) ADRL -中等范围的地址读取伪指令

3) LDR -大范围的地址读取伪指令

4) NOP -空操作伪指令

(二) Thumb 指令集

1. Thumb 指令集和 ARM指令集的区别

2. Thumb 存储器访问指令

1) LDR/ STR -加载 /存储指令

2) PUSH/ POP -寄存器入栈 /出栈指令

3) LDMIA/ STMIA -多寄存器加载 /存储指令

3. Thumb 数据处理指令

1) 数据传送指令

a) MOV -数据传送指令

b) MVN -数据非传送指令

c) NEG -数据取负指令

2) 算术逻辑运算指令

a) ADD -加法运算指令

b) SUB -减法运算指令

c) ADC -带进位加法指令

d) SBC -带进位减法指令

e) MUL -乘法运算指令

f) AND -逻辑“与”

g) ORR -逻辑“或”

h) EOR -逻辑“异或”

i) BIC -位清除指令

j) ASR -算术右移指令

k) LSL -逻辑左移指令

l) LSR -逻辑右移指令

m) ROR -循环右移指令

3) 比较指令

a) CMP -比较指令

b) CMN -负数比较指令

c) TST -位测试指令

4. Thumb 分支指令

1) B -分支指令

2) BL -带连接的分支指令

3) BX -带状态切换的分支指令

5. Thumb 杂项指令

1) SWI -软中断指令

6. Thumb 伪指令

1) ADR -小范围的地址读取伪指令

2) LDR -大范围的地址读取伪指令

3) NOP -空操作伪指令

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

各个平台arm-none-linux-gnueabi交叉编译工具下载

发表于 2021-01-03

arm-none-linux-gnueabi-gcc是 Codesourcery 公司(目前已经被Mentor收购)基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM系统中所有环节的代码,包括裸机程序、u-boot、Linux kernel、filesystem和App应用程序。使用时,按照主机平台,可以下载以下任一版本中的一个,结果是一样的:

  • Linux解压版:在Linux主机(如Ubuntu、RedHat等)直接解压即可使用。推荐方式!
  • Linux安装版:在Linux主机下执行后按照提示安装后使用。
  • Windows解压版:在Windows系统下解压后使用,但是需要MingW32。
  • Windows安装版:在Windows系统下安装后使用。
  • RPM安装版:RedHat系统安装包,新版本不提供该类安装包。
  • 源码版:交叉编译器源代码,一般很少用到。

为方便国内用户下载使用,我们从Codesourcery(Mentor)官网下载了所有版本的arm-none-linux-gnueabi-gcc编译器,并放到国内云盘上提供下载。因为版本很多,可能难以选择,但是我们建议您使用最新版本,即有最新~标志的下载项。

1 Linux解压版

gnu-csl-arm-2005Q1B-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2006q1-3-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2006q1-6-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2006q3-26-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2007q1-10-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2007q1-21-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2007q3-51-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2008q1-126-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2008q3-41-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2008q3-72-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2009q1-176-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2009q1-203-arm-none-linux-gnueabi-i686-pc-linux-gnu.bz2 —— 下载
arm-2009q3-67-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2010.09-50-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2010q1-202-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2011.03-41-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2012.09-64-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2013.05-24-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2013.11-33-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 —— 下载
arm-2014.05-29-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 最新~推荐!—— 下载

2 Linux安装版

arm-2006q3-26-arm-none-linux-gnueabi.bin —— 下载
arm-2007q1-10-arm-none-linux-gnueabi.bin —— 下载
arm-2007q1-21-arm-none-linux-gnueabi.bin —— 下载
arm-2007q3-51-arm-none-linux-gnueabi.bin —— 下载
arm-2008q1-126-arm-none-linux-gnueabi.bin —— 下载
arm-2008q3-41-arm-none-linux-gnueabi.bin —— 下载
arm-2008q3-72-arm-none-linux-gnueabi.bin —— 下载
arm-2009q1-176-arm-none-linux-gnueabi.bin —— 下载
arm-2009q1-203-arm-none-linux-gnueabi.bin —— 下载
arm-2009q3-67-arm-none-linux-gnueabi.bin —— 下载
arm-2010.09-50-arm-none-linux-gnueabi.bin —— 下载
arm-2010q1-202-arm-none-linux-gnueabi.bin —— 下载
arm-2011.03-41-arm-none-linux-gnueabi.bin —— 下载
arm-2011.09-70-arm-none-linux-gnueabi.bin —— 下载
arm-2012.03-57-arm-none-linux-gnueabi.bin —— 下载
arm-2012.09-64-arm-none-linux-gnueabi.bin —— 下载
arm-2013.05-24-arm-none-linux-gnueabi.bin —— 下载
arm-2013.11-33-arm-none-linux-gnueabi.bin —— 下载
arm-2014.05-29-arm-none-linux-gnueabi.bin —— 最新~ 下载

3 Windows Mingw32 版

gnu-csl-arm-2005Q1B-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2006q3-26-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2007q1-10-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2007q1-21-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2007q3-51-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2008q1-126-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2008q3-41-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2008q3-72-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2009q1-176-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2009q1-203-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2009q3-67-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2010.09-50-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2010q1-202-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2011.03-41-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2011.09-70-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2012.03-57-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2012.09-64-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2013.05-24-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2013.11-33-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 下载
arm-2014.05-29-arm-none-linux-gnueabi-i686-mingw32.tar.bz2 —— 最新~ 下载

4 Windows安装版

gnu-csl-arm-2005Q1B-arm-none-linux-gnueabi.exe —— 下载
arm-2006q1-3-arm-none-linux-gnueabi.exe —— 下载
arm-2006q1-6-arm-none-linux-gnueabi.exe —— 下载
arm-2006q3-26-arm-none-linux-gnueabi.exe —— 下载
arm-2007q1-10-arm-none-linux-gnueabi.exe —— 下载
arm-2007q1-21-arm-none-linux-gnueabi.exe —— 下载
arm-2007q3-51-arm-none-linux-gnueabi.exe —— 下载
arm-2008q1-126-arm-none-linux-gnueabi.exe —— 下载
arm-2008q3-41-arm-none-linux-gnueabi.exe —— 下载
arm-2008q3-72-arm-none-linux-gnueabi.exe —— 下载
arm-2009q1-176-arm-none-linux-gnueabi.exe —— 下载
arm-2009q1-203-arm-none-linux-gnueabi.exe —— 下载
arm-2009q3-67-arm-none-linux-gnueabi.exe —— 下载
arm-2010.09-50-arm-none-linux-gnueabi.exe —— 下载
arm-2010q1-202-arm-none-linux-gnueabi.exe —— 下载
arm-2011.03-41-arm-none-linux-gnueabi.exe —— 下载
arm-2011.09-70-arm-none-linux-gnueabi.exe —— 下载
arm-2012.03-57-arm-none-linux-gnueabi.exe —— 下载
arm-2012.09-64-arm-none-linux-gnueabi.exe —— 下载
arm-2013.05-24-arm-none-linux-gnueabi.exe —— 下载
arm-2013.11-33-arm-none-linux-gnueabi.exe —— 下载
arm-2014.05-29-arm-none-linux-gnueabi.exe —— 最新~ 下载

5 PRM安装版

arm-arm-none-linux-gnueabi-2007q3-51.i686.rpm —— 下载

6 源码

gnu-csl-arm-2005Q1B-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2006q1-3-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2006q1-6-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2006q3-26-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2007q1-10-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2007q1-21-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2007q3-51-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2008q1-126-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2008q3-41-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2008q3-72-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2009q1-176-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2009q1-203-arm-none-linux-gnueabi.src.tar.bz2.bz2 —— 下载
arm-2009q3-67-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2010.09-50-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2010q1-202-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2011.03-41-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2011.09-70-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2012.03-57-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2012.09-64-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2013.05-24-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2013.11-33-arm-none-linux-gnueabi.src.tar.bz2 —— 下载
arm-2014.05-29-arm-none-linux-gnueabi.src.tar.bz2 —— 最新~ 下载

Mentor官方下载地址(需要注册,注册之后官方会发送一个下载地址到邮箱里面):http://www.mentor.com/embedded-software/sourcery-tools/sourcery-codebench/evaluations/。

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

android设备上如何运行C语言原生程序

发表于 2021-01-03

前言

咱们知道android设备可以直接运行apk,或者使用dalvikvm指令运行dex文件中的程序, 但是这两者本质上使用的语言都是java或者smali,

如果需要执行C语言程序,我们通常需要借助NDK编译成so库, 那有没有不使用NDK而直接编译c原生程序然后在安卓设备中运行的方式呢?

交叉编译

C语言是一门跨平台的语言,语言跨平台,但是程序不跨平台

也就是说在windows平台下编译的程序只能在windows平台上执行,linux亦是如此, 为什么会出现这种情况呢?

除了操作系统导致的文件格式不一致之外,本质上是因为不同平台cpu使用的架构不一致导致

目前主流的三大cpu架构x86,ARM,MIPS,每个架构都对应着属于自己的一套汇编指令集, 我们编写的程序,最终都会解码成汇编指令被cpu所执行,

那么在windows平台上进行本机编译的C语言代码,会根据当前平台的cpu架构和操作系统生成只能在当前平台运行的程序

如果我想在windows平台编译出能在linux系统或者在ARM设备中运行的程序, 那该怎么办呢?

这个时候我们就要使用交叉编译工具, 那这种编译的模式就不叫本机编译了,而叫做交叉编译

为什么需要交叉编译

场景:

  • 你正在编译一款linux应用,但你手上只有一台windows电脑,这个时候交叉编译能帮上大忙
  • 目标设备环境不允许,比如单片机, 内存和性能无法支持程序的编译
  • 一款配备新硬件的机器问世, 需要借助交叉编译来编译它的系统和编译器等

交叉编译工具

目前主流的交叉编译工具有GCC和Clang, 接下来主要介绍GCC的使用方法:

首先

交叉编译工具链的命名规则为:arch [-vendor] [-os] [-(gnu)eabi]

  • arch – 体系架构,如ARM,MIPS
  • vendor – 工具链提供商
  • os – 目标操作系统
  • eabi – 嵌入式应用二进制接口(Embedded Application Binary Interface)

根据对操作系统的支持与否,ARM GCC可分为支持和不支持操作系统,

示例

  • arm-none-eabi:这个是没有操作系统的,适用于ARM架构裸机,工具链提供商未知,嵌入式应用二进制接口,这个工具不支持那些跟操作系统关系密切的函数,比如fork(2)。他使用的是newlib这个专用于嵌入式系统的C库。
  • arm-none-linux-eabi:用于Linux的,ARM架构,使用Glibc

如果是裸机编译,由于没有操作系统进行文件识别翻译,因此编译出来的大部分是面向cpu内核的机器码文件

单片机编译后的文件就是如此

回到主题

我需要用C语言编写一个能在安卓设备上运行的程序, 首先安卓设备大多采用的是ARM架构cpu, 系统采用的是linux系统,

如果我们想在安卓设备中进行本机编译的话,实现起来比较困难, 那么咱们可以借助交叉编译工具,实现在windows平台上编译出能够在ARM+Linux平台上的可执行程序

这里我们使用 arm-none-linux-gnueabi-gcc 这款交叉编译工具

工具下载:

各个平台arm-none-linux-gnueabi交叉编译工具下载

或者

linux平台arm-none-linux-gnueabi工具直链下载

第一步 源码编写

新建test.c文件

1
2
3
4
5
6
7
#include <stdio.h>  

int main()
{
printf("Hello world!\n");
return 0;
}

第二步 源码编译

在命令窗口执行bin包下的arm-none-linux-gnueabi-gcc指令

1
arm-none-linux-gnueabi-gcc test.c -o test -static

因为Android 的 Linux 内核没有标准IO库函数,因此我们采用静态编译的方式进行编译 末尾-static必须要有

第三步 将编译后的可执行文件传至安卓设备

1
adb push test /data/

第四步 执行文件

直接在控制台输入文件名即可执行

1
./test

如果提示权限拒绝,那么执行以下指令:

1
chmod 777 test

至此 程序正常运行 控台输出Hello world!

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

x86汇编语言之DI和SI寄存器

发表于 2020-12-25

概念

SI(source index)是源变址寄存器
DI(destination index)是目的变址寄存器
它们和[bx]类似,用于存放偏移地址

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
assume cs:code,ds:data

;需求:字符串复制到...位置
data segment
db 'welcome to masm!'
db '................'
data ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax

mov si,0
mov di,16

mov cx, 8
copydata:
mov dx,ds:[si]
mov es:[di],dx
add si,2
add di,2
loop copydata

code ends


end start

改成栈复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
assume cs:code,ds:data

;需求:字符串复制到...位置
data segment
db 'welcome to masm!'
db '................'
data ends

stack segment
db 32 dup(0)
stack ends

code segment
start:
mov ax,data
mov ds,ax
mov es,ax

mov ax,stack
mov ss,ax
mov sp,32

mov si,0
mov di,16

mov cx, 8
copydata:
push ds:[si]
pop es:[di]
add si,2
add di,2
loop copydata

code ends


end start

偏移地址组合

  1. ds:[bx+数字]

    1
    ds:[bx+数字] ;如果写成[数字+bx],那么相当于[bx]+数字,两者意思不一样
  2. ds:[bx+si]和ds:[bx+si+数字]

  3. ds:[bx+di]和ds:[bx+di+数字]

偏移地址组合实际应用

  1. 需求:首字母变成大写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
assume cs:code ,ds:data
; 需求:首字母变成大写
data segment
db '1. file '
db '2. edit '
db '3. search '
data ends


code segment
start:
mov ax,data
mov ds,ax
mov es,ax

mov bx,0
mov si,3

mov cx,3
upLetter:
mov dl,ds:[bx+si]
and dl,11011111B
mov es:[bx+si],dl
add bx,16
loop upLetter

code ends

end start
  1. 需求:所有字母变成大写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
assume cs:code ,ds:data,ss:stack
; 需求:所有字母变成大写
data segment
db '1. file '
db '2. edit '
db '3. sear '
data ends

stack segment
db 32 dup(0)
stack ends


code segment
start:
mov ax,data
mov ds,ax
mov es,ax

mov ax,stack
mov ss,ax
mov sp,32

mov bx,0
mov si,3

mov cx,3
upRow: ;使用两层循环, 因为内循环需要用到cx和si寄存器,避免数据被覆盖,使用栈临时保护
push cx
push si

mov cx,4
upLetter:
mov dl,ds:[bx+si]
and dl,11011111B
mov es:[bx+si],dl
inc si
loop upLetter

add bx,16
pop si
pop cx
loop upRow

code ends

end start

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

x86汇编语言之段空间大小的对齐

发表于 2020-12-24

段所占空间大小和特点

以8086为例,假如声明一个段,不论是数据段,栈段还是代码段, 一旦段里面有内容,那么会从一个新的段地址开始开辟空间,如果代码进行了分段处理,那么就会形成16字节对齐的现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assume ds:data
;场景一
data segment
;如果没有内容,则不开辟空间, 被编译器忽略
data ends

;场景二
data segment
dw 55FFH,55FFH ;后面的数据用0补足16个字节
data ends

;场景三
data segment
dw 55FFH,55FFH
db 'hello' ;以上占用连续的内存空间 55FFH 55FFH hello
data ends

;场景四
data segment
dw 55FFH,55FFH,55FFH,55FFH,55FFH,55FFH,55FFH,55FFH,55FFH,55FFH,55FFH,55FFH;超过16个字节,那么开辟16的倍数也就是32个字节的空间, 以此类推
data ends
段空间占用计算公式:

如果段中的数据占用N个字节,则程序加载后,该段实际占用空间为:

1
(N/16+1)*16

为什么说是现象呢

本质并不是因为段固定占用n16字节,而是因为段必须从一个新的地址段开始开辟空间,这就导致了我们认为*段一次最少拉升16字节的内存空间,必须为16的倍数**, 原因看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

data segment
db 'hello'
data ends

start:
call print
mov ax,2000H

print:
mov bx,3000H
mov cx,4000H
ret


end start

内存分布如下:

我们发现data段并没有独占16个字节空间,而是让数据从一个新的16字节地址开始存入,段的作用是让数据在内存中的排列按照一定的布局进行排列,方便我们进行计算读取, 但是使用段的话明显会占用更多的内存空间

各个段之间内存排列分布

数据段,栈段,代码段 他们在内存中开辟的空间是根据代码由上到下依次分布的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
assume ds:data,cs:code,ss:stack

data segment
dw 66FF
data ends
;假如数据段的段地址是2000H 那么栈段的段地址为2001H, 代码段的段地址为:2002H 依次排列

stack segment
dw 77FF
stack ends

code segment
mov ax,data
mov ds,ax

mov ax,stack
mov ss,ax

code ends

;数据段--->栈段--->代码段

假如数据段的段地址是2000H 那么栈段的段地址为2001H, 代码段的段地址为:2002H 依次排列

如果我将各个段的代码位置调整一下,那么所在内存的位置也会跟着发生改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume ds:data,cs:code,ss:stack

;假如栈段的段地址是2000H 那么代码段的段地址为2001H, 数据段的段地址为:2002H 依次排列

stack segment
dw 77FF
stack ends

code segment
mov ax,data
mov ds,ax

mov ax,stack
mov ss,ax

code ends


data segment
dw 66FF
data ends

;栈段--->代码段--->数据段

也就是说各个段的内存分布不是固定的, 和代码的编写有关系

如果不加start标记的话,汇编代码默认由上往下执行,cs+ip从上往下,所过之处,全部被当做代码处理, 因此即便你在数据段中存放指令,甚至打入代码起始标签也没有问题:

1
2
3
4
5
6
7
8
9
10
assume cs:code,ds:data

data segment
mov ax,2000H ;第一步 执行
data ends
;由于一个段所占空间为16的倍数,后面空位补0,被当做为指令对待,因此当在数据段中执行16个字节代码后,由于段与段之间内存是连续分布的,如果ip的值刚好指向了代码段,那么紧接着执行代码段中的内容,如果ip没有指向代码段,则不执行, 这个情况是不可控的

code segment
mov ax,3000H ;ip偏移16个字节后 执行
code ends

如果在数据段中加上代码起始标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
assume cs:code,ds:data

data segment
dw 55FFH
start:
mov ax,2000H ;第一步 先执行 如果没有标记,则从dw开始当做代码执行
data ends


code segment
mov ax,3000H ;ip偏移16个字节后 执行
code ends
end start

段和段地址之间的关系

每个段会独占一个栈地址

1
2
3
4
5
6
7
assume cs:code,ds:data

mov ax,3000H ;占用三个字节空间

data segment
age dw 'hello'
data ends

内存分布如下:

咱们会发现数据段,并不是从0100:0003开始开辟16个字节的空间,而是新起一个段地址从0101:0000开始开辟, 也就是说一个段占用独立的一个栈地址

假如把段去掉:

1
2
3
4
5
assume cs:code,ds:data

mov ax,3000H ;占用三个字节空间

age dw 'hello'

则内存分布如下:

那么代码会依次连续进行累加填充

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

x86汇编语言之大小写快速转换

发表于 2020-12-23

分析

大写字母 abcd对应的ascii码以及二进制:

1
2
3
4
A  41H  0100 0001B
B 42H 0100 0010B
C 43H 0100 0011B
D 44H 0100 0100B

小写字母 abcd对应的ascii码以及二进制:

1
2
3
4
a  61H  0110 0001B
b 62H 0110 0010B
c 63H 0110 0011B
d 64H 0110 0100B

发现规律

大写转小写,只需二进制和0010 0000B进行或运算即可:

1
or 0010 0000B ;大写变成小写, 小写依然还是小写

小写转大写,只需二进制和1101 1111B进行与运算即可:

1
and 1101 1111B ;小写变成大写, 大写依然还是大写

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
assume cs:code ,ds:data

data segment
db 'ABcDEF'
db 'abCdef'
data ends


code segment

start:
mov ax ,data
mov ds,ax
mov bx,0

mov ax,data
mov es,ax

mov cx,6

downLetter: ;大写转小写
mov dl,ds:[bx]
or dl,00100000B
mov es:[bx],dl
inc bx
loop downLetter


mov cx,6
mov bx,6
upLetter: ;小写转大写
mov dl,ds:[bx]
and dl,11011111B
mov es:[bx],dl
inc bx
loop upLetter

mov ah,4ch
int 21h


code ends
end start

以上代码可使用偏移地址组合方式[bx+x]进行优化,结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
assume cs:code ,ds:data

data segment
db 'ABcDEF'
db 'abCdef'
data ends


code segment

start:
mov ax ,data
mov ds,ax
mov bx,0

mov ax,data
mov es,ax

mov cx,6

changeLetter:
;大写转小写
mov dl,ds:[bx]
or dl,00100000B
mov es:[bx],dl
;小写转大写
mov dl,ds:[bx+6]
and dl,11011111B
mov es:[bx+6],dl
inc bx
loop changeLetter


mov ah,4ch
int 21h


code ends
end start

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

x86汇编语言之8086语法和指令集

发表于 2020-12-23

x86汇编语法

  1. 注释
1
;我是注释
  1. 变量取值和赋值(传送指令)
1
2
3
4
5
6
;赋值
mov ax,2000H ;将十六进制2000赋值给十六位寄存器ax 相当于ax=2000H


;取值
mov bx,ax ;将ax中的值取出赋值给bx 相当于bx=ax

存放的数据大小根据使用的寄存器而定, 比如ax是16位寄存器,最大只能存放16位数据,也就是4位十六进制数据

十六进制数据不能以字母开头, 前面需加上0 否则编译报错

  1. 函数声明

结构如下:

1
2
3
函数名:
函数体
ret ;结尾标记

示例:

1
2
3
4
5
6
print:	;函数名
mov dx,offset str
mov ah, 9ch
int 21h

ret ;函数结尾标记
  1. 函数调用

x86架构中使用关键指令call

x86架构汇编示例:

1
2
3
4
5
6
7
8
9
10
11
		call print ;调用print函数

;退出程序
mov ah, 4ch
int 21h

print: ;函数名
mov dx,offset str;获取别名对应数据的偏移地址
mov ah, 9ch ;9h表示调用显存 从dx总读取偏移地址对应的数据
int 21h
ret
  1. 字符串的定义

    起因:如果直接将字符串赋值给通用寄存器,会出现以下两个问题:

    • 字符顺序是反着的
    • 最多只能存放两个字符
    • 无法获取到数据地址,不能堆字符串进行修改

    为了解决这个问题,需要使用另外一种方式,定义字符串

    首先:需要先在内存中申请一块空间,可以使用伪指令db和dw

    1
    2
    3
    4
    5
    db-->define byte  定义字节 读写一个数据,偏移量加1
    dw-->define word 定义字 读写一个数据,偏移量加2


    dd-->define double word 定义双字, 读写一个数据 偏移量加4
    • 示例

      1
      2
      db 'hello'  ;占用五个字节的内存空间
      dw 'hello' ;占用六个字节的内存空间 跟偏移量有关系

    如果定义数字,使用dw每个数字占用两个字节的空间, 字符串比较特殊,并不是每个字符占用两个字节,而是总长度必须是2的倍数

  1. 字符串的获取

    获取字符串的数据,首先要获取到数据所对应的内存地址

    那怎么获取已经定义好的地址呢?

    第一步尝试: 给数据添加别名

    1
    2
    3
    4
    5
    6
    7
    str db 'hello'

    start:
    mov bx,str ;别名中存放的是偏移地址


    end start

    别名中存放的是偏移地址,但是光有偏移地址还不行,还需要段地址, 段地址+偏移地址=实际物理地址,别名默认从ds寄存器中读取段地址, 但是我们并没有给ds寄存器赋过值, 这就导致我们无法获取正确的数据,因为我们不知道正确的段地址是多少?

    那字符串段地址从哪里获取呢?

    • 方法一: 直接从内存中找(仅限于调试,实际开发肯定不行)

    • 方法二:使用段进行包裹, 段能给我们提供一个段地址(正解)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      data segment
      str db 'hello'
      data ends
      ;使用段进行包裹, 可以借助段名称获取段地址
      start:
      mov ax,data
      mov ds,ax
      mov bx,str

      end start
  2. 对内存中的数据进行读写

    从内存中一次读取数据的多少,取决于寄存器的容器大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      data segment
    str dw 'hello' ;如果定义多个数据 使用逗号进行分隔
    data ends

    start:
    mov ax,data
    mov ds,ax
    mov ax,str ;如果从内存中读取数据,是根据寄存器大小来读取,16位寄存器则一次性读取16位数据,8位al则一次性读取八位数据

    end start

思考:为什么以下写法报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
;报错1
data segment
str db 'hello ' ;改成dw则不报错
data ends
start:
mov ax,data
mov ds,ax
mov ax,str ;

end start

;报错2
data segment
str dw 'hello '
data ends

start:
mov ax,data
mov ds,ax
mov al,str ; ;改成mov ax,str 或者mov al,b.str 则不报错

end start



;报错3
mov ax,bl ;宽度不匹配 以上两个也是同样的问题

内存数据的读写是从低往高进行读写

上面使用db或者dw定义数据的方式,定义数据的同时就已经定义好了数据所在的物理地址, 如果我们想要从指定的内存地址中写入或者读取数据的话,需要借助段寄存器来实现 在8086中给我们提供了DS SS CS ES四个寄存器,理论上你使用哪一个都行,但是由于系统默认读取DS寄存器中的数据当做段地址,所以我们一般使用DS进行数据的段地址管理

  • 如何从指定内存中读取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    ;假设我们需要从0710:0000这个物理地址中读取数据,然后存放到寄存器中

    ;错误写法1:
    start:
    mov ax,0710H:0000H ;没有这种语法
    end start


    ;错误写法2:
    start:
    mov ds,0710H ;段寄存器不能直接赋值,必须借助通用寄存器
    mov ax,ds:[0]
    end start

    ;正确写法:
    start:
    mov ax,0710H
    mov ds,ax
    mov ax,ds:[0] ;实际物理地址 段地址+偏移地址 ===>ds:[xxx] 表示从该地址取数据
    end start
  • 如何往指定内存中写入数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    ;假设我们需要将数据写入0710:0000这个物理地址中

    ;错误写法1:
    start:
    mov ax,3333H
    mov 0710:0000,ax ;没有这种语法
    end start


    ;错误写法2:
    data segment
    str dw 'he'
    data ends

    start:
    mov ax,data
    mov ds,ax
    mov ds:[0],ds:str ;必须借助通用寄存器进行赋值
    end start



    ;正确写法1:
    data segment
    str dw 'he'
    data ends

    start:
    mov ax,data
    mov ds,ax
    mov ax,ds:str ;str-==>[xx] ds:[xxx]
    mov ds:[0],ax
    end start

    ;正确写法2:
    start:
    mov ax,0710H
    mov ds,ax;指定需要写入数据的段地址

    mov ax,3333H ;将3333H当做数据
    mov ds:[0],ax
    end start

    ;正确写法3:
    start:
    mov ax,0710H
    mov ds,ax;指定需要写入数据的段地址

    mov ds:[0],3333H ;可以直接将数据写入 最多写入十六位的数据
    end start

补充:往内存中写入数据是字节宽度还是字型宽度取决于寄存器的宽度也就是 mov ds:[0],ax 或者数据的大小:mov ds:[0] 30H

1
2
3
4
5
; 以下是指定数据占用空间的大小 可以实现8位数据占用16个字节的空间的目的

mov byte ptr ds:[0],1 ;前面加上byte ptr关键字 表示往指定内存中写入字节型数据1

mov word ptr ds:[0],1 ;往指定内存中写入字型数据1 也就是在内存中占用两个字节空间

​
8. 字符串修改和替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
;需求1 : 将内存中he修改为wo

data segment
str dw 'he'
newstr dw 'wo'
data ends


start:
mov ax,data
mov ds,ax

mov ax,ds:str
mov ds:newstr,ax

end start



;需求2 : 将内存中hello 修改为wowowo

data segment
str dw 'hello '
newstr dw 'wowowo'
data ends


start:
mov ax,data
mov ds,ax

mov ax,ds:str
mov ds:newstr,ax

mov ax,ds:str+2
mov ds:newstr+2,ax

mov ax,ds:str+4
mov ds:newstr+4,ax

end start

分段写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

data segment
str dw 'he'

data ends

newData segment
newstr dw 'wo'
newData ends

code segment
start:
mov ax,data
mov ds,ax
mov ax,newData
mov es,ax

mov ax,ds:str
mov es:newstr,ax

code ends

end start
  1. Loop循环指令

    类似于高级语言中的while循环, 系统默认从cx寄存器中读取数据作为循环的条件,当cx中的值cx-1大于零时循环执行一次代码

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    ;需求 : 将内存中wowowo修改为hello 

    data segment
    str dw 'hello '
    newstr dw 'wowowo'
    data ends


    start:
    mov ax,data
    mov ds,ax

    mov bx,0
    mov cx,3
    replace:
    mov ax,ds:[bx]
    mov ds:[bx+6],ax
    add bx,2
    loop replace

    end start


    ;需求 : 将内存中wowowo修改为hello

    data segment
    dw 'aaa'
    str dw 'hello '
    newstr dw 'wowowo'
    data ends


    start:
    mov ax,data
    mov ds,ax

    mov bx,offset str
    mov cx,3
    replace:
    mov ax,ds:[bx]
    mov ds:[bx+6],ax
    add bx,2
    loop replace

    end start

    加减运算指令add和sub

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    add ax,2 ;ax=ax+2
    sub ax,2 ;ax=ax-2


    sub/add 通用寄存器,数值 ;add/sub ax,2
    sub/add 通用寄存器,通用寄存器 ;add/sub ax,bx
    sub/add 内存地址,通用寄存器 ;add/sub ds:[0],bx
    sub/add 通用寄存器,内存地址 ;add/sub ax,ds:[0]


    ;错误写法
    sub/add 内存地址,内存地址 ;;add/sub ds:[0],ds:[3] 不允许这样写
  1. 中断

    顾名思义,程序运行到一半暂时断开,官方一点说就是,由于软件或者硬件信号,使得cpu暂停当前任务,转而执行另一段子程序

    可以形象理解为游戏中暂时搁置主线任务临时去完成支线任务

    中断的分类:

    • 外中断 (硬中断):由外部设备(比如网卡,或者硬盘 键盘 鼠标)引发的中断,比如当网卡收到数据包的时候,就会发出一个中断

    • 内中断(软中断):由执行的中断指令产生的,可以通过程序控制触发

    我们接下来要学习的是内中断知识,如果我们想要通过代码发出一个中断,那么需要使用中断指令int

    1
    int 21h ;执行中断码为21H的中断指令

    cpu接收到中断信号后,暂停当前正在执行的指令,临时去执行中断码对应的内容

    中断码不止一个,每个码代表着不同的含义,部分中断码列表如下:

    中断 功能 入口参数 出口参数
    INT16 键盘输入 AH=0H读键盘 AH=10读扩展键盘 AH=键盘扫描码 AL=字符ascii码
    INT20 程序正常退出 CS=PSP段地址
    INT21 系统功能调用 AH=功能号
    INT22 程序结束处理
    INT23 Ctrl-Break处理 AL=0(忽略)
    INT24 严重错误处理 AL=驱动器号 AL=1(重试)AL=2(通过INT 23H终止)Cy=1出错
    INT25 绝对磁盘读 CX=读入扇区数DX=起始逻辑扇区数DS:BX=缓冲区地址AL=驱动器号 Cy=0正确
    INT26 绝对磁盘写 CX=写盘扇区数DX=起始逻辑扇区数DS:BX=缓冲区地址
    INT27 驻留退出 CS=PSP段地址DX=程序末地址+1
**二、DOS功能调用**

**功能号在AH中,并设好其余的入口参数,向DOS发出INT21H命令,最后获得出口参数。**

| 调用号 | 功能                                                         | 入口参数                                                     | 出口参数                                                     |
| ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 00H    | 程序终止                                                     | CS=PSP段地址                                                 |                                                              |
| 01H    | 键盘输入字符                                                 |                                                              | AL=输入的字符                                                |
| 02H    | 显示输出                                                     | DL=显示的字符                                                |                                                              |
| 03H    | 串行设备输入                                                 |                                                              | AL=输入的字符                                                |
| 04H    | 串行设备输出                                                 | DL=输出的字符                                                |                                                              |
| 05H    | 打印输出                                                     | DL=输出的字符                                                |                                                              |
| 06H    | 直接控制台I/O                                                | DL=0FFH(输入请求)DL=字符(输出请求)                           | AL=输入的字符                                                |
| 07H    | 直接控制台I/O(不显示输入)                                    |                                                              | AL=输入的字符                                                |
| 08H    | 键盘输入字符(无回显)                                         |                                                              | AL=输入的字符                                                |
| 09H    | 显示字符串                                                   | DS:DX=缓冲区首址                                             |                                                              |
| 0AH    | 输入字符串                                                   | DS:DX=缓冲区首址                                             |                                                              |
| 0BH    | 检查标准输入状态                                             |                                                              | AL=00无按键AL=0FFH有按键                                     |
| 0CH    | 清除输入缓冲区并执行指定的标准输入功能                       | AL=功能号(01/06/07/08/0AH)DS:DX=缓冲区(0AH功能)              | AL=输入的数据(功能01/06/07/08)                               |
| 0DH    | 初始化磁盘状态                                               |                                                              |                                                              |
| 0EH    | 选择缺省的驱动器                                             | DL=驱动器号(0=A,1=B..)                                       | AL=逻辑驱动器数                                              |
| 0FH    | 打开文件                                                     | DS:DX=未打开的FCB首址                                        | AL=00成功,0FFH失败                                           |
| 10H    | 关闭文件                                                     | DS:DX=打开的FCB首址                                          | AL=00成功,0FFH失败                                           |
| 11H    | 查找第一匹配目录                                             | DS:DX=未打开的FCB首址                                        | AL=00成功,0FFH失败                                           |
| 12H    | 查找下一匹配目录                                             | DS:DX=未打开的FCB首址                                        | AL=00成功,0FFH失败                                           |
| 13H    | 删除文件                                                     | DS:DX=未打开的FCB首址                                        | AL=00成功,0FFH失败                                           |
| 14H    | 顺序读                                                       | DS:DX=打开的FCB首址                                          | AL=00成功,01文件结束02缓冲区太小03缓冲区不满                 |
| 15H    | 顺序写                                                       | DS:DX=打开的FCB首址                                          | AL=00成功,01盘满02缓冲区太小                                 |
| 16H    | 创建文件                                                     | DS:DX=未打开的FCB首址                                        | AL=00成功0FFH目录区满                                        |
| 17H    | 文件换名                                                     | DS:DX=被修改的FCB首址                                        | AL=00成功,0FFH未找到目录项或文件重名                         |
| *18H   | 保留未用                                                     |                                                              |                                                              |
| 19H    | 取缺省驱动器号                                               |                                                              | AL=驱动器号(0=A,1=B..)                                       |
| 1AH    | 设置磁盘缓冲区DTA                                            | DS:DX=磁盘缓冲区首址                                         |                                                              |
| *1BH   | 取缺省驱动器的磁盘格式信息                                   |                                                              | AL=每簇的扇区数CX=每扇区的字节数DX=数据区总簇数-1DS:BX=介质描述字节 |
| *1CH   | 取指定驱动器的磁盘格式信息                                   | DL=驱动器号(0=缺省,1=A..)                                    | AL=每簇的扇区数CX=每扇区的字节数DX=数据区总簇数-1DS:BX=介质描述字节 |
| *1DH   | 保留未用                                                     |                                                              |                                                              |
| *1EH   | 保留未用                                                     |                                                              |                                                              |
| *1FH   | 取缺省驱动器的DPB                                            |                                                              | DS:BX=DPB首址                                                |
| *20H   | 保留未用                                                     |                                                              |                                                              |
| 21H    | 随机读一个记录                                               | DS:DX=打开的FCB首址                                          | AL=00成功,01文件结束02缓冲区太小03缓冲区不满                 |
| 22H    | 随机写一个记录                                               | DS:DX=打开的FCB首址                                          | AL=00成功,01盘满02缓冲区太小                                 |
| 23H    | 取文件大小                                                   | DS:DX=未打开的FCB首址                                        | AL=00成功,0FFH失败                                           |
| 24H    | 设置随机记录号                                               | DS:DX=打开的FCB首址                                          |                                                              |
| 25H    | 设置中断向量                                                 | AL=中断号DS:DX=中断程序入口                                  |                                                              |
| *26H   | 创建新的PSP                                                  | DS:DX=新的PSP段地址                                          |                                                              |
| 27H    | 随机读若干记录                                               | DS:DX=打开的FCB首址CX=要读入的记录数                         | AL=00成功,01文件结束AL=02缓冲区太小AL=03缓冲区不满CX=读入的块数 |
| 28H    | 随机写若干记录                                               | DS:DX=打开的FCB首址CX=要写入的记录数                         | AL=00成功,01盘满AL=02缓冲区太小AL=03缓冲区不满CX=已写的块数  |
| 29H    | 分析文件名                                                   | AL=分析控制标记DS:SI=要分析的字符串ES:DI=未打开的FCB首址     | AL=00未通配符01有通配符0FFH驱动器字母无效ES:DI=未打开的FCB   |
| 2AH    | 取系统日期                                                   |                                                              | CX=年(1980-2099)DH=月,DL=日,AL=星期(0=星期日)                |
| 2BH    | 置系统日期                                                   | CX=年,DH=月,DL=日                                            | AL=00成功,0FFH失败                                           |
| 2CH    | 取系统时间                                                   |                                                              | CH=时(0-23),CL=分,DH=秒,DL=百分之几秒                        |
| 2DH    | 置系统时间                                                   | CX=时,分DX=秒,百分秒                                         | AL=00成功,0FFH失败                                           |
| 2EH    | 设置/复位校验开关                                            | AL=0关闭,1打开                                               |                                                              |
| 2FH    | 取磁盘传输地址DTA                                            |                                                              | ES:BX=DTA首地址                                              |
| 30H    | 取DOS版本                                                    |                                                              | AL,AH=DOS主、次版本                                          |
| 31H    | 结束并驻留                                                   | AL=返回码,DX=内存大小                                        |                                                              |
| *32H   | 取指定驱动器的DPB                                            |                                                              | DS:BX=DPB首址                                                |
| 33H    | 取或置Ctrl-Break标志                                         | AL=0:取,1:置,DL=标志                                         | DL=标志(取功能)0:关1:开                                      |
| *34H   | 取DOS中断标志                                                |                                                              | ES:BX=DOS中断标志                                            |
| 35H    | 取中断向量地址                                               | AL=中断号                                                    | ES:BX=中断程序入口                                           |
| 36H    | 取磁盘的自由空间                                             | DL=驱动器号(0=缺省,1=A                                       | AX=FF驱动器无效其它每簇扇区数BX=自由簇数CX=每扇区字节数BX=文件区所占簇数 |
| *37H   | 取/置参数分隔符取/置设备名许可标记                           | AL=0:取分隔符,1:置分隔符,DL=分隔符2:取许可标记3:置许可标记,DL=许可标记 | DL=分隔符(功能0)DL=许可标记(功能2)                           |
| 38H    | 取国家信息                                                   | AL=0,DS:DX=缓冲区首址                                        |                                                              |
| 39H    | 创建子目录                                                   | DS:DX=路径字符串                                             | CF=0成功,1失败,AX=错误码                                     |
| 3AH    | 删除子目录                                                   | DS:DX=路径字符串                                             | CF=0成功,1失败,AX=错误码                                     |
| 3BH    | 设置子目录                                                   | DS:DX=路径字符串                                             | CF=0成功,1失败,AX=错误码                                     |
| 3CH    | 创建文件                                                     | DS:DX=带路径的文件名CX=属性1-只读2-隐蔽4-系统                | CF=0成功,AX=文件号CF=1失败,AX=错误码                         |
| 3DH    | 打开文件                                                     | DS:DX=带路径的文件名AL=方式0-读1-写2-读写                    | CF=0成功,AX=文件号CF=1失败,AX=错误码                         |
| 3EH    | 关闭文件                                                     | BX=文件号                                                    | CF=0成功CF=1失败,AX=错误码                                   |
| 3FH    | 读文件或设备                                                 | BX=文件号CX=字节数                                           | CF=0成功DX:AX=新的指针位置                                   |
| 40H    | 写文件或设备                                                 | DS:DX=缓冲区首址                                             | CF=1失败,AX=错误码                                           |
| 41H    | 删除文件                                                     | DS:DX=带路径的文件名                                         | CF=0成功,1失败,AX=错误码                                     |
| 42H    | 移动文件指针                                                 | AL=方式0-正向1-相对2-反向BX=文件号,CX:DX=移动的位移量        | CF=0成功,DX:AX=新的文件指针CF=1失败,AX=错误码                |
| 43H    | 取/置文件属性                                                | AL=0:取1:置,CX=新属性DS:DX=带路径的文件名                    | CX=属性(功能0)1-只读2-隐蔽4-系统20H-归档                     |
| 44H    | 设备输入/输出控制:设置/取得与打开设备的句柄相关联信息,或发送/接收控制字符串至设备句柄 | AL=0/1取/置设备信息2/3读/写设备控制通道4/5同功能2/36/7取输入/输出状态BX=句柄(功能0-3,6-7)BL=驱动器号(功能4-5)CX=字节数(功能2-5)DS:DX=缓冲区(功能2-5) | CF=0成功DX=设备信息(功能0)AL=状态(功能6/7)0未准备,1准备AX=传送的字节数(功能2-5) |
| 45H    | 复制文件号(对于一个打开的文件返回一个新的文件号)             | BX=文件号                                                    | CF=0成功,AX=新文件号CF=1失败,AX=错误码                       |
| 46H    | 强行复制文件号                                               | BX=现存的文件号,CX=第2文件号                                 | CF=0成功,1失败AX=错误码                                      |
| 47H    | 取当前目录                                                   | DL=驱动器号DS:SI=缓冲区首址                                  | CF=0成功,1失败AX=错误码                                      |
| 48H    | 分配内存                                                     | BX=所需的内存节数                                            | CF=0成功,AX=分配的段数,CF=1失败,AX=错误码BX=最大可用块大小   |
| 49H    | 释放内存                                                     | ES=释放块的段值                                              | CF=1失败,AX=错误码                                           |
| 4AH    | 修改分配内存                                                 | ES=修改块的段值BX=新长度(以节为单位)                         | CF=1失败,AX=错误码BX=最大可用块大小                          |
| 4BH    | 装载程序运行程序                                             | AL=0装载并运行1获得执行信息3装载但不运行DS:DX=带路径的文件名ES:BX=装载用的参数块 | CF=1失败,AX=错误码                                           |
| 4CH    | 带返回码的结束                                               | AL=进程返回码                                                |                                                              |
| 4DH    | 取由31H/4CH带回的返回码                                      |                                                              | AL=进程返回码AH=类型码,0-正常结束1-由Ctrl-Break结束2-由严重设备错误而结束3-由调用31H而结束 |
| 4EH    | 查找第一个匹配项                                             | DS:DX=带路径的文件名CX=属性                                  | CF=1失败,AX=错误码                                           |
| 4FH    | 查找下一个匹配项                                             |                                                              | CF=1失败,AX=错误码                                           |
| *50H   | 建立当前的PSP段地址                                          | BX=PSP段地址                                                 |                                                              |
| *51H   | 读当前的PSP段地址                                            |                                                              | BX=PSP段地址                                                 |
| *52H   | 取DOS系统数据区首址                                          |                                                              | ES:BX=DOS数据区首址                                          |
| *53H   | 为块设备建立DPB                                              | DS:SI=BPB,ES:DI=DPB                                          |                                                              |
| 54H    | 取校验开关设定值                                             |                                                              | AL=标志值(0:关,1:开)                                         |
| *55H   | 由当前PSP建立新PSP                                           | DX=PSP段地址                                                 |                                                              |
| 56H    | 文件换名                                                     | DS:DX=带路径的旧文件名ES:DI=带路径的新文件名                 | CF=1失败,AX=错误码                                           |
| 57H    | 取/置文件时间及日期                                          | AL=0/1取/置,BX=文件号CX=时间,DX=日期                         | CF=0成功,CX=时间,DX=日期                                     |
  1. 打印字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    data segment
    str db 'hello pangshu!$' ;$ 表示结尾标记

    data ends
    code segment

    start:
    mov ax,data
    mov ds,ax

    ;业务逻辑代码
    mov dx,offset str;获取别名对应数据的偏移地址
    mov ah, 9h ;9h表示调用显存 从dx总读取偏移地址对应的数据
    int 21h

    ;退出程序
    mov ah, 4ch
    int 21h
    code ends
    end start
  1. 除法指令div

    格式:

    1
    div 寄存器  ;div bx   该寄存器表示除数

    公式是被除数÷除数=商……余数

被除数需要预先存放在ax寄存器中,相除后商存放在ax寄存器中,余数存放在dx寄存器中

1
2
3
4
5
6
mov ax,2000 ;定义被除数
mov bx,300 ;定义除数
div bx ; 进行除法运算

mov ds:[0],ax ;将商存放至内存中
mov ds:[2],dx ;将余数存放至内存中

因为ax和dx需要存放被除数和余数, 因此除数一般放在bx,cx,或者内存中

由于16位寄存器最多只能存放16位数据,假如被除数数值超过十六位,那么被除数低16位存放在ax寄存器中,高十六位存放在dx寄存器中

1
2
3
4
5
6
7
mov dx,20   ;定义被除数高16位
mov ax,2000 ;定义被除数低16位 就是1407D0H
mov bx,300 ;定义除数
div bx ; 进行除法运算

mov ds:[0],ax ;将商存放至内存中
mov ds:[2],dx ;将余数存放至内存中
  1. 乘法指令

    1
    2
    3
    4
    5
    6
    mov ax,100
    mov bx,1000
    mul bx ;相乘

    mov ds:[0],ax ;将积低16位存放至内存中
    mov ds:[2],dx ;将积高16位存放至内存中

段寄存器

  1. 数据和指令的区别

内存中存放的数据都是十六进制的数据,如果这些十六进制数据被标记为数据,那么cpu在读取的时候,读到多少就是多少,如果被标记为了指令,那么cpu会将这些十六进制转成指令进行执行

cpu只是个无情的计算机器,它无法自动区分数据和指令,标记需要我们告诉cpu

那么 如何给数据进行标记呢?

  1. 段寄存器的种类
1
2
3
4
DS---->data segment
SS---->stack segment
CS---->code segement
ES---->extra segement

都是用于存放段地址的

1
2
实际物理地址=段地址*16:偏移地址 10*10=100  
71001 = 0710*16:0001

DS寄存器用于存放数据的段地址,那么数据所对应的偏移地址可以使用bx寄存器进行存放,使用DS寄存操作的内存数据都被当成是纯数据,里面存的是什么,读出来的就是什么

1
mov ax,3000H  ;这行代码会转成16进制的数据存放到内存中 这些数据所对应的段地址默认放在ds寄存器中 ,使用ds进行读取的时候 读取的是该指令对应的16进制数据, 而不是被还原成了指令进行执行

CS寄存器用于存放指令所在的段地址,IP寄存器存放的是当前正在执行的指令所对应的偏移地址,所有使用CS:IP进行操作的内存数据都被当成是指令对待,读取的时候会将16进制数据转成对应的指令并执行

1
2
3
4
5
mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000
mov ax,2000H ;假如这条这条指令数据所对应的物理地址为0710:0003


;如果我想跳过第一条指令 直接执行第二条指令 那么直接修改CS:IP的值为0710:0003即可

SS寄存器用于存放栈空间对应的段地址,所有被SS操作的内存空间都被当成栈空间进行对待,你想让哪部分内存空间当作栈空间,完全取决于开发者,sp寄存器存放栈空间偏移量,ss和sp配合使用

1
2
3
mov ax,0710H
mov ss,ax
mov sp,20 ;将0710:0000~0710:0014H 这20个字节的内存空间定义为栈空间

ES寄存器一般用于DS的替补,DS被占用无法使用时,临时使用ES替代,用法和DS一致

栈空间的操作

栈段里面存放也是数据和数据段无异,只不过数据排列的方式不一样,正常的排列方式是数据从低地址往高地址进行偏移存放,读取数据也是从低到高,而栈则是写入数据从高到低进行偏移,读取数据从低地址到高地址

由于这个特性,所以我们在定义一块空间作为栈空间使用时,都会先往高地址偏移一段空间

栈存储特点:

  • 一次读写两个字节的数据

  • 数据高地址往低地址逆序偏移存放

  1. 栈空间的声明

    前面提到过,使用ss寄存器进行标记的空间为栈空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    data segment
    db 0,0,0,0,0,0,0,0 ;定义数据相当于是开辟了一块未标记内存空间,这块空间可以当作是数据空间也可以栈或者指令空间,却决于该段地址是由哪个段寄存器进行管理的
    data ends

    code segment
    start:
    mov ax,data
    mov ss,ax ;该空间被ss指向,因此被当作是栈空间 如果是被CS指向则被当成指令空间,里面存放的数据都会被当成指令进行执行
    mov sp,8
    code ends
    end start
  2. 往栈空间中写入数据

    使用push指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    data segment
    db 0,0,0,0,0,0,0,0
    data ends

    code segment
    start:
    mov ax,data
    mov ss,ax
    mov sp,8

    mov ax,2000H
    push ax ;一次写入两个字节的数据,如果使用al编译报错
    code ends
    end start
  3. 从栈空间读取数据

    使用pop指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    data segment
    db 0,0,0,0,0,0,0,0
    data ends

    code segment
    start:
    mov ax,data
    mov ss,ax
    mov sp,8

    mov ax,2000H
    push ax

    pop bx ;一次读取两个字节的数据,如果使用bl编译报错
    code ends
    end start

    思考:栈空间和数据段空间,里面存放的都是数据,那么是否可以使用SS来存放数据段的段地址呢,答案是可以的, 当SS充当数据段的时候,读写操作和DS一样,SS:[N],如果充当栈段的时候,读写操作时候时候pop和push,

    由于pop和push默认以SS寄存器中的数据当做栈段地址,因此不能使用其他段寄存器充当栈段寄存器

操控显存输出字符串

前面咱们介绍过使用中断的方式输出字符串, 今天我们学习一种不使用中断的方式实现字符串的打印

在8086的内存地址结构中,B8000H~BFFFFH这部分的内存区域为显存区域,一旦向这个地址空间写入数据,cpu会从0号偏移地址开始读取数据然后显示输出, (每写入一次数据就从0开始读取一次)

代码尝试:

1
2
3
4
5
6
7
8
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl
end start

在这块区域中,每个字符固定占用两个字节的空间,也就是ds:[0]和ds:[1]存放一个字符的信息,前者存放字符具体的内容,后者存放字符对应的颜色

比如:

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00000100B ;让字符以红色显示
mov ds:[1],dl
end start

字符颜色的设置规则:

1
0 0 0 0 0 0 0 0  ;用8个二进制位表示字符属性

从高往低数,第一个二进制位表示是否显示闪烁痕迹

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,10000000B ;保留字符闪烁痕迹
mov ds:[1],dl
end start

第234个二进制位表示字符背景颜色 分别代表:RGB,即red、green、blue

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,01000000B ;背景颜色设为红色
mov ds:[1],dl
end start

第5个二进制位表示字符是否高亮

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00001100B ;字符颜色设置为红色 并且高亮显示
mov ds:[1],dl
end start

第678个二进制位表示字符本身的颜色 分别代表:RGB,即red、green、blue

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00000111B ;背景颜色设为白色 系统默认颜色是白色
mov ds:[1],dl
end start

由于cpu会从0号偏移地址开始读取数据然后显示输出,因此假如你直接在6号偏移地址写入字符数据, 那么前面三个数据会以占位形式存在

1
2
3
4
5
6
7
8
9
10
11
start:
mov ax,0B800H
mov ds,ax


mov dl,'a'
mov ds:[0],dl

mov dl,00000111B
mov ds:[6],dl ;输出结果为" a"
end start

字符串打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
data segment
str db 'hello pangshu'
endstr db ''
data ends

code segment

start:

mov ax,data
mov ds,ax
mov ax,0B800H
mov es,ax

mov cx ,offset endstr-str
mov bx,0
mov si,0

print:
mov dl,ds:[si]
mov es:[bx],dl

mov dl,00000111B ;背景颜色设为白色 系统默认颜色是白色
mov es:[bx+1],dl
inc si
add bx,2
loop print

code ends
end start

借助字符不断刷新显示的特性,可用让字符动画显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;让字符从左往右移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,0
mov cx,30
print:
mov es:[bx],' '
mov dl,'a'
mov es:[bx+2],dl
add bx,2
loop print

code ends
end start

屏幕默认显示80x25个字符,全屏显示106x38个字符,那么可以根据这个特性,让字符上下移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
;让字符从上往下移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,0
mov cx,25
print:
mov es:[bx],'a'
mov dl,' '
mov es:[bx-160],dl
add bx,160 ;为什么是160而不是80 以内一个字符占两个字节的空间, 80个字符总共偏移了160
loop print

code ends
end start



;让字符从下往上移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,160*24
mov cx,25
print:
mov es:[bx],'a'
mov dl,' '
mov es:[bx+160],dl
sub bx,160 ;为什么是160而不是80 以内一个字符占两个字节的空间, 80个字符总共偏移了160
loop print

code ends
end start

另外, 让字符斜着移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;让让字符斜着移动
code segment

start:
mov ax,0B800H
mov es,ax

mov bx,0
mov cx,25
print:
mov es:[bx],'a'
mov dl,' '
mov es:[bx-161],dl
add bx,161 ;向右斜加偏移量 向左斜减偏移量
loop print

code ends
end start

补充: 在8086中系统提供了一个显示服务(Video Service)中断供我们使用,使用10H这个中断码也可以打印带有颜色属性的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;示例1:
mov ah,2 ;放置光标
mov bh,0 ;第0页
mov dh,5 ;行号
mod dl,12 ;列号
int 10H


;示例2:
mov ah,9 ;在光标的位置显示字符
mov al,'a' ;字符
mov bl,11001010B ;颜色
mov bh,0 ;第0页
mov cx,3 ;重复显示3次
int 10H

使用键盘输入控制字符移动

使用16号中断码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
;使用键盘控制字符移动
code segment

start:
mov ax,0B800H
mov es,ax
mov bx,0
mov cx,30

scan:
mov ah, 00H
int 16H
cmp al,61H ;判断两个值是否相等
jne scan2 ;jmp not equal 如果两者不相等 则跳转到scan2 否则往下执行
call left
jmp scan
scan2:
cmp al,64H
jne scan3
call right
jmp scan
scan3:
cmp al,77H
jne scan4
call top
jmp scan
scan4:

cmp al,73H
jne scan

call down
jmp scan

right:
mov es:[bx],' '
mov dl,'a'
mov es:[bx+2],dl
add bx,2
ret


left:
mov es:[bx],' '
mov dl,'a'
mov es:[bx-2],dl
sub bx,2
ret

top:
mov es:[bx],' '
mov dl,'a'
mov es:[bx-160],dl
sub bx,160
ret



down:
mov es:[bx],' '
mov dl,'a'
mov es:[bx+160],dl
add bx,160
ret

code ends
end start

内存读写的另外一种简便方法

直接使用别名+[偏移量]的形式获取或者修改已经定义好的数据,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;需求:将第二个字符串最后一个o替换成第一个字符串中的e
data segment
str dw 'hello '
newstr dw 'wowowo'
data ends


;以前的写法:
code segment
start:
mov ax,data
mov ds,ax
mov al,ds:b.str+1
mov ds:b.newstr+5,al
code ends
end start

;更加简便的写法:
code segment
start:
mov ax,data
mov ds,ax
mov al ,b.str[1] ;使用b.或者直接定义db数据 实现对单个字符进行控制
mov b.newstr[5],al
code ends
end start

mov ax ,str 中的str相当于是str[0],这也是为什么咱们能够直接通过别名来获取第一个字符的原因

前面我们只介绍了b.str[0],表示读取一个字节的数据,另外一种w.str[0]表示读取一个字的数据

别名str中存放的数据量更db和dw有关, 如果是db那么别名默认取一个字节数据,如果dw 别名默认取两个字节数据, 可以使用b.和w.进行数据量的调节

一维数组的定义

数组本质上就是多个数据的集合,在内存中按照一定顺序排列,角标即为每个数据的偏移值,如果使用db进行数据的定义那么两个数据之间的偏移值为1个字节,如果使用dw指令定义数据,则偏移值为2个字节

1
2
3
4
5
6
7
data segment
db 3 dup(1) ;批量定义三个连续的数据 每个数据的偏移量为1 在内存中的排列为 111
data ends

data segment
db 3 dup(1,2,3) ;批量定义三个连续的数据 每个数据的偏移量为1 在内存中的排列为 123123123
data ends

数组的打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data segment
array db 128 dup(0)
data ends

code segment
start:
mov ax,data
mov ds,ax

mov array[0],1
mov array[1],2
mov array[2],3


code ends
end start

外中断

外中断 (硬中断):由外部设备(比如网卡,或者硬盘 键盘 鼠标)引发的中断,比如当网卡收到数据包的时候,就会发出一个中断

中断屏蔽:

cpu监听到外部中断时,第一步需要先判断IF标志位的值,如果为1则执行,如果为0则屏蔽

为什么还有中断屏蔽这么一说? 因为有些重复的外部中断并不需要响应,或者cpu正在在执行非常重要的一段指令,这些指令不能中断,一旦中途调开可能会造成系统崩溃,那么在这种情况下需要先将IF标志位置为0,屏蔽所有可屏蔽的外部信号,等这段指令执行完后,再讲IF还原回1, 这个IF标志位相当于是一个监听外部信号的开关

为了方便IF标志位的修改,8086提供了相应的指令方面我们使用

1
2
3
cli  ;相当于IF=0

sti ;相当于IF=1

键盘中断

当我们按下键盘中的一个键,那么键盘会向cpu发出一个中断信号,cpu接收到信号后判断标志位IF是否为1,如果为1,则执行这个中断

当松开按键时,同样会发出一个中断信号,也就是说点击一个键实际产生了两个中断信号

以上这一系列过程涉及到几个问题:

​ 1. 键盘是一个外部设备,电信号发出时,cpu怎么知道这个信号是来自键盘而不是鼠标的呢?

​ 2. cpu又如何知道你按下的是哪一个键?

解答1:

​ cpu除了可以对寄存器 和内存进行数据读写之外,还可以读写端口的数据,电脑上一共有65535个端口,每个端口相当于是一个数据通道,当外部键盘借助USB接口接入电脑被驱动识别后,势必有一个端口与其相连进行数据通信, 在8086中这个端口号为60H

那么如何使用汇编读写端口中的数据呢?

1
2
3
in al,60H ; 使用in指令 从60h这个端口读取一个字节到al寄存器中去

out 60h,al ; 使用out指令 al寄存器中的数据写入60H端口

从端口读写数据必须使用ax或者al寄存器进行交互

解答2:

​ 键盘每个键位所对应的字符都有与之对应的扫描码一一映射,不同厂商键盘硬件对应的扫描码可能不一致,它们最终都会转成相应的ascii码

键位扫描码参考表如下:

key mark(Hex)按下 break(Hex)松开 描述
NumLock 45 c5 break = mark + 0x80
/ e0 35 e0 b5 由 e0 引导出 extend scan code
* 37 b7 break = mark + 0x80
- 4a ca 同上
7/Home 47 c7 同上
8/Up 48 c8 同上
9/PgUp 49 c9 同上
4/Left 4b cb 同上
5 4c cc 同上
6/Right 4d cd 同上
1/End 4f cf 同上
2/Down 50 d0 同上
3/PgDn 51 d1 同上
0/Ins 52 d2 同上
./Del 53 d3 同上
+ 4e ce 同上
Enter e0 1c e0 9c
Scroll Lock 46 c6 同上
Pause/Break e1 1d 45 e1 9d c5 * 同上
Insert e0 52 e0 d2 同上
Home e0 47 e0 c7 同上
Page Up e0 49 e0 c9 同上
Delete e0 53 e0 d3 同上
End e0 4f e0 cf 同上
Page Down e0 51 e0 d1 同上
left e0 46 e0 c6 同上
right e0 4d e0 cd 同上
up e0 48 e0 c8 同上
down e0 50 e0 d0 同上
01 81 scan code
F1 3b bb 同上
F2 3c bc 同上
F3 3d bd 同上
F4 3e be 同上
F5 3f bf 同上
F6 40 c0 同上
F7 41 c1 同上
F8 42 c2 同上
F9 43 c3 同上
F10 44 c4 同上
F11 57 d7 同上
F12 58 d8 同上
~/· 29 a9
0f 8f
3a ba
2a aa
!/1 02 82
q 10 90
a 1e 9e
z 2c ac
@/2 03 83
w 11 91
s 1f 9f
x 2d ad
#/3 04 84
e 12 12
d 20 a0
c 2e ae
$/4 05 85
r 13 93
f 21 a1
v 2f af
%/5 06 86
t 14 94
g 22 a2
b 30 b0
^/6 07 87
y 15 95
h 23 a3
n 31 b1
&/7 08 88
u 16 96
j 24 a4
m 32 b2
*/8 09 89
i 17 97
k 25 a5
</, 33 b3
(/9 0a 8a
o 18 98
l 26 a6
>/. 34 b4
)/0 0b 8b
p 19 99
:/; 27 a7
?// 35 b5
_/- 0c 8c
{/[ 1a 9a
“/‘ 28 a8
36 b6
+/= 0d 8d
}/] 1b 9b
1c 9c
1d 9d
|/\ 2b ab
38 b8
0e 8e
39 b9

如果是控制键ctrl shift 则将其转变成状态字节, 记录到0040:0017这个内存空间中,也就是说当我们按下控制键 这个位置的数据会发生相应的改变

磁盘读写

  1. 概念

磁盘构造:

1
2
3
4
5
一面=80个磁道

一个磁道=18个扇区

一个扇区=512byte
  1. 中断

    使用13H号中断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    ;读取磁盘中的数据到0:200H这个内存中
    mov ax,0
    mov es,ax
    mov bx,200h

    mov al,1 ;读取的扇区数
    mov ch,0 ;磁道号
    mov cl,1 ;扇区号
    mov dl,0 ;驱动器号 软驱a, b
    mov dh,2 ;面号
    mov ah,2 ;2表示读取 3表示写入
    int 13H


    ;将0:200H这个内存中数据写入软盘
    mov ax,0
    mov es,ax
    mov bx,200h

    mov al,1 ;读取的扇区数
    mov ch,0 ;磁道号
    mov cl,1 ;扇区号
    mov dl,0 ;驱动器号 软驱a, b
    mov dh,2 ;面号
    mov ah,3 ;写入
    int 13H

转移指令jmp ,jcxz和retf

如果我们想要实现以下效果:

1
2
3
4
5
mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000
mov ax,2000H ;假如这条这条指令数据所对应的物理地址为0710:0003


;如果我想跳过第一条指令 直接执行第二条指令 那么直接修改CS:IP的值为0710:0003即可

修改方法:

  • 调试器直接修改

  • 使用jmp指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    jmp 0100h:8H
    mov bx,3333H
    mov ax,2000H


    ;如果在同一个段中 可以直接使用jmp+偏移地址的形式
    jmp 8H
    mov bx,3333H
    mov ax,2000H

    或者使用标记

    1
    2
    3
    4
    jmp me
    mov bx,3333H
    me:
    mov ax,2000H
  • 使用jcxz指令

    jcxz (jmp cx zero):条件转移指令,功能和jmp一样,只是需要满足条件,也就是当cx寄存器中的值为0时,进行跳转

    1
    2
    3
    4
    5
    mov cx,0
    jcxz me
    mov bx,3333H
    me:
    mov ax,2000H
  • 使用retf指令

    retf需要配合栈进行使用,当程序执行到retf这条指令时,会连续从栈中pop两次数据,第一次的数据赋值给CS,第二次的数据赋值给IP,那么如果我们想要跳转到指定的指令,需要将该指令的段地址和偏移地址分别push进栈中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    stack segment
    db 128 dup(0)
    stack ends

    code segment
    start:
    mov ax,stack
    mov ss,ax
    mov sp,128

    mov ax,0710H ;指定段地址
    push ax
    mov ax,0003H ;指定偏移地址
    push ax

    retf ;程序跳转到0710:0003H这个位置
    code ends
    end start

Call和Ret进阶

call在执行时会先将下一条指令所对应的ip地址入栈,然后修改ip的值实现跳转, ret指令执行的时候,将ip地址pop出来进行跳转

1
call s ;标号里面存放的是ip偏移地址 如果写成call 3H  那么意思就是跳转到CS:0003h这个位置

call Far ptr 执行时会将下一条指令做对应的cs和ip都入栈,retf指令执行的时候,将ip和cs值pop出来进行跳转

ret和call配套使用,retf和call Far ptr 配套使用

可以通过标号(函数名称)之间数值相减计算函数体代码所占用的内存空间大小

1
2
3
4
5
6
7
8
9
10
11
12
code segment          
mov ax,func1
mov bx, func2
sub bx,ax ;计算函数func1占用的内存空间大小

func1:
mov ax,3333H
ret
func2:
mov bx,4444H
ret
code ends

直接从内存中获取ip地址然后跳转

1
call word ptr ds:[0] ;ds:[0]存放ip值

直接从内存中获取cs和ip地址然后跳转

1
2
3
call dword ptr ds:[0] ;ds:[0]存放的是ip值, ds:[2]存放的是cs值

;这种方式同样会将cs和ip入栈 可以配合retf使用

以上两种直接从内存中获取cs:ip的方式对于jmp指令同样有效

call指令和jmp指令的区别

  • jmp指令仅仅只是修改了cs:ip的值
  • call指令除了修改cs:ip的值之外,还将下一条指令的ip值入栈,方便ret指令跳转调用

iret指令:

iret指令执行,将ip和cs值pop出来进行跳转,同时还执行了popf,相当于执行了以下三步操作

1
2
3
pop ip
pop cs
popf

内中断进阶

我们利用中断码段可以调用系统的功能,也就是被系统封装好的子程序

中断既然能够引导cpu临时去执行子程序,那么势必是更改了cs:ip的值,也就是在内存中存放了这个子程序的入口指令地址,通过int关键字找出来并跳转。这里有两个先决条件,一个是子程序必须提前编写好存放在内存中,二是将入口地址存放在内存的某个位置

当程序执行到int指令时,根据中断码计算出程序入口所在的物理地址,然后然后取出来赋值给cs:ip

那么怎么通过中断码计算呢?

比如 int 0h 会从0000:0000这个地址开始找出四个字节数据,由高地址往低地址分别为段地址和偏移地址

由于每个中断码需要占用四个字节空间,因此int 2H 从0000:0004开始找,以此类推

公式为:

1
2
IP=中断码*4
CS=中断码*4+2

配合咱们之前学的call指令 int 9h 可以用以下指令替代:

1
2
3
4
5
6
int 9h 

;相当于一下三行代码
mov ax,0000H
mov ds,ax
call dword ptr ds:[9h*4]

编写自定义中断

  1. 编写子程序

    1
    2
    3
    child:
    mov ax,3322H
    retf
  1. 将子程序入口地址值存放到中断码对应的内存位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    code segment
    start:
    call write

    mov ah,4cH
    int 21h


    ;=======子程序======
    child:
    mov ax,3322H
    retf
    endd: nop
    ;=======子程序======


    ;=======将子程序入口地址写入内存======
    write:

    ;子程序所在的段地址
    mov ax,cs
    ;子程序所在的偏移地址
    mov bx,child

    mov cx,0000H
    mov es,cx
    mov es:[9h*4],bx
    mov es:[9h*4+2],ax
    ret
    code ends
    end start
  2. 使用中断码调用子程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    code segment
    start:
    call write

    ;测试中断
    int 9H

    mov ah,4cH
    int 21h


    ;=======子程序======
    child:
    mov ax,3322H
    retf
    endd: nop
    ;=======子程序======


    ;=======将子程序入口地址写入内存======
    write:

    ;子程序所在的段地址
    mov ax,cs
    ;子程序所在的偏移地址
    mov bx,child

    mov cx,0000H
    mov es,cx
    mov es:[9h*4],bx
    mov es:[9h*4+2],ax
    ret
    code ends
    end start

音乐播放

声音由震动产生,不同频率对应不同的声音,也就是音高

单个音高的声音不能叫音乐,只能叫音频,

而音乐是由不同时长的不同音高组合而成,比如100Hz的声音持续1秒紧接着200Hz的声音持续半秒,如此反复循环,那么在听感上就是一首音乐

在音乐演奏的世界里,为了表示音高,一般采用记谱的方式来代替频率,从而方便音乐家们交流,但是对于计算机而言,计算机不能识别简谱也不能识别五线谱

计算机的发声原理

电信号—–>驱动——->扬声器

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
data segment
;音高
freq dw 262,262,262,196
dw 330,330,330,262
dw 262,330,392,392
dw 349,330,294
dw 294,330,349,349
dw 330,294,330,262
dw 262,330,294,196
dw 247,294,262,-1
;时长
time dw 3 dup(12,12,25,25),12,12,50
dw 3 dup(12,12,25,25),12,12,50
data ends

stack segment
db 100H dup(0)
stack ends

code segment
start:
;主程序
mov ax,stack
mov ss,ax
mov sp,100H
mov ax,data
mov ds,ax

lea si,freq
lea di,time

play:
mov dx,[si]
cmp dx,-1
je end_play
call sound
add si,2
add di,2
jmp play

end_play:
mov ah,4cH
int 21H
sound:
push ax
push dx
push cx
;定时器的设置
mov al,0b6h
out 43H,al
mov dx,12H
mov ax,34dcH
div word ptr[si]
out 42h,al
mov al,ah
out 42h,al
;设置8255芯片,控制扬声器开关
in al,61h
mov ah,al
or al ,3
out 61h,al
;延时一定的时长
mov dx,[di]
wait1:
mov cx,28000
delay:
nop
loop delay
dec dx
jnz wait1
;恢复扬声器端口原值
mov al,ah
out 61h,al
pop cx
pop dx
pop ax
ret
code ends
end start

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

x86汇编语言之and和or指令以及ascii码

发表于 2020-12-22

And指令

是逻辑运算指令, 按位进行与运算, 也就是需要转成二进制进行运算, 1代表真, 0代表假

1
2
3
4
mov al,1111 1111B ;或者写成十六进制FFH的形式也可
and al,0000 1111B ;将al中的值和0000 1111B进行与运算, 然后将结果赋值给al

;结果为 0000 1111B

or指令

按位进行或运算

1
2
3
4
mov al,1111 1111B 
or al,0000 1111B

;结果为1111 1111B

ascii码

1
mov al 'a' ;该行代码将字符a所对应的ascii码传入al寄存器中

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

汇编语言之MIPS汇编

发表于 2020-12-21

简介

咱们知道x86架构cpu用于PC端和工作站较多,ARM架构cpu常见于手机和单片机,那么MIPS架构的cpu主要在哪些设备可以找到它们的身影呢?

  • 中国龙芯
  • PS游戏机

学习环境搭建

  • 安装JDK, 主要用于运行mips模拟器mars
  • MARS模拟器:https://courses.missouristate.edu/KenVollmar/mars/download.htm

寄存器

在mips中通用寄存器用$开头表示,一共有32个

寄存器编号 寄存器名 寄存器用途
$0 $zero 永远返回0
$1 $at 保留寄存器
$2-$3 $v0-$v1 一般用于存储表达式或者函数的返回值(value的简写)
$4-$7 $a0-$a3 参数寄存器(Argument简写)
$8-$15 $t0-$t7 一般用于存储临时变量(temp简写)
$16-$23 $s0-$s7 存放子函数调用过程需要被保留的数据(saved values)
$24-$25 $t8-$t9 属性同$t0-$t7
$26-$27 $k0-$k1 一般存储中断函数返回值
$28 $gp GlobalPointer简写
$29 $sp 栈指针,指向栈顶(Stack Pointer简写)
$30 $s8/$fp (Save / Frame Pointer)帧指针
$31 $ra 一般用于存储函数返回地址(return address简写)

寄存器编号和别名一一对应,同一个寄存器可以有两种不同表示方法:$0或者$zero

  • program counter (PC) 无法直接修改,通过跳转指令可以改动
  • HI 和 LO :这两个寄存器特别用来保存乘法、除法、乘法累加的结果。

MIPS汇编中的分段处理

1
2
3
.data #数据段

.text #代码段

传送指令

  1. 加载立即数指令 li

li(load immediate) :用于将立即数传送给寄存器

1
li $t0,1  ;十六进制数据使用0x前缀表示
  1. 加载地址指令 la

la(load address) :用于将地址传送至寄存器中, 多用于通过地址获取数据段中的地址

1
2
3
4
5
.data 
msg: .ascii "hello world"

.text
la $a0,msg # 将字符串数据所在的地址赋值给$a0寄存器
  1. 寄存器数据传送指令move

用于将一个寄存器中的数据传送至另一个寄存器当中

1
move $t0,$t1  # 将寄存器$t1中的数据传送至$t0

系统服务指令 syscall

在C语言中输出文本可以使用printf函数,但是汇编中没有printf这么一说,如果想要输出文本,需要借助syscall指令

如果想要输出一个数字1,那么syscall指令从$a0寄存器中取出需要输出的数据

因此, 你在执行syscall指令之前需要将数据提前放入$a0之中:

1
2
li $a0,1
syscall

同时,还需要指定输出的数据类型,数据类型的指定保存在$v0寄存器中

1
2
# $v0=1, syscall--->print_int
# $v0=4, syscall--->print_string

$v0存入1,表示syscall将$a0中的数据当做数字输出

$v0存入4,表示syscall将$a0中的数据当做数据的地址,然后输出对应的数据

syscall指令读写对照表

Service Code in $v0 Arguments Result
print integer 1 $a0 = integer to print
print float 2 $f12 = float to print
print double 3 $f12 = double to print
print string 4 $a0 = address of null-terminated string to print
read integer 5 $v0 contains integer read
read float 6 $f0 contains float read
read double 7 $f0 contains double read
read string 8 $a0 = address of input buffer $a1 = maximum number of characters to read See note below table
sbrk (allocate heap memory) 9 $a0 = number of bytes to allocate $v0 contains address of allocated memory
exit (terminate execution) 10
print character 11 $a0 = character to print See note below table
read character 12 $v0 contains character read
open file 13 $a0 = address of null-terminated string containing filename $a1 = flags $a2 = mode $v0 contains file descriptor (negative if error). See note below table
read from file 14 $a0 = file descriptor $a1 = address of input buffer $a2 = maximum number of characters to read $v0 contains number of characters read (0 if end-of-file, negative if error). See note below table
write to file 15 $a0 = file descriptor $a1 = address of output buffer $a2 = number of characters to write $v0 contains number of characters written (negative if error). See note below table
close file 16 $a0 = file descriptor
exit2 (terminate with value) 17 $a0 = termination result See note below table
Services 1 through 17 are compatible with the SPIM simulator, other than Open File (13) as described in the Notes below the table. Services 30 and higher are exclusive to MARS.
time (system time) 30 $a0 = low order 32 bits of system time $a1 = high order 32 bits of system time. See note below table
MIDI out 31 $a0 = pitch (0-127) $a1 = duration in milliseconds $a2 = instrument (0-127) $a3 = volume (0-127) Generate tone and return immediately. See note below table
sleep 32 $a0 = the length of time to sleep in milliseconds. Causes the MARS Java thread to sleep for (at least) the specified number of milliseconds. This timing will not be precise, as the Java implementation will add some overhead.
MIDI out synchronous 33 $a0 = pitch (0-127) $a1 = duration in milliseconds $a2 = instrument (0-127) $a3 = volume (0-127) Generate tone and return upon tone completion. See note below table
print integer in hexadecimal 34 $a0 = integer to print Displayed value is 8 hexadecimal digits, left-padding with zeroes if necessary.
print integer in binary 35 $a0 = integer to print Displayed value is 32 bits, left-padding with zeroes if necessary.
print integer as unsigned 36 $a0 = integer to print Displayed as unsigned decimal value.
(not used) 37-39
set seed 40 $a0 = i.d. of pseudorandom number generator (any int). $a1 = seed for corresponding pseudorandom number generator. No values are returned. Sets the seed of the corresponding underlying Java pseudorandom number generator (java.util.Random). See note below table
random int 41 $a0 = i.d. of pseudorandom number generator (any int). $a0 contains the next pseudorandom, uniformly distributed int value from this random number generator’s sequence. See note below table
random int range 42 $a0 = i.d. of pseudorandom number generator (any int). $a1 = upper bound of range of returned values. $a0 contains pseudorandom, uniformly distributed int value in the range 0 = [int] [upper bound], drawn from this random number generator’s sequence. See note below table
random float 43 $a0 = i.d. of pseudorandom number generator (any int). $f0 contains the next pseudorandom, uniformly distributed float value in the range 0.0 = f 1.0 from this random number generator’s sequence. See note below table
random double 44 $a0 = i.d. of pseudorandom number generator (any int). $f0 contains the next pseudorandom, uniformly distributed double value in the range 0.0 = f 1.0 from this random number generator’s sequence. See note below table
(not used) 45-49
ConfirmDialog 50 $a0 = address of null-terminated string that is the message to user $a0 contains value of user-chosen option 0: Yes 1: No 2: Cancel
InputDialogInt 51 $a0 = address of null-terminated string that is the message to user $a0 contains int read $a1 contains status value 0: OK status -1: input data cannot be correctly parsed -2: Cancel was chosen -3: OK was chosen but no data had been input into field
InputDialogFloat 52 $a0 = address of null-terminated string that is the message to user $f0 contains float read $a1 contains status value 0: OK status -1: input data cannot be correctly parsed -2: Cancel was chosen -3: OK was chosen but no data had been input into field
InputDialogDouble 53 $a0 = address of null-terminated string that is the message to user $f0 contains double read $a1 contains status value 0: OK status -1: input data cannot be correctly parsed -2: Cancel was chosen -3: OK was chosen but no data had been input into field
InputDialogString 54 $a0 = address of null-terminated string that is the message to user $a1 = address of input buffer $a2 = maximum number of characters to read See Service 8 note below table $a1 contains status value 0: OK status. Buffer contains the input string. -2: Cancel was chosen. No change to buffer. -3: OK was chosen but no data had been input into field. No change to buffer. -4: length of the input string exceeded the specified maximum. Buffer contains the maximum allowable input string plus a terminating null.
MessageDialog 55 $a0 = address of null-terminated string that is the message to user $a1 = the type of message to be displayed: 0: error message, indicated by Error icon 1: information message, indicated by Information icon 2: warning message, indicated by Warning icon 3: question message, indicated by Question icon other: plain message (no icon displayed) N/A
MessageDialogInt 56 $a0 = address of null-terminated string that is an information-type message to user $a1 = int value to display in string form after the first string N/A
MessageDialogFloat 57 $a0 = address of null-terminated string that is an information-type message to user $f12 = float value to display in string form after the first string N/A
MessageDialogDouble 58 $a0 = address of null-terminated string that is an information-type message to user $f12 = double value to display in string form after the first string N/A
MessageDialogString 59 $a0 = address of null-terminated string that is an information-type message to user $a1 = address of null-terminated string to display after the first string N/A

使用syscall指令输出helloworld示例:

1
2
3
4
5
6
7
8
.data 

msg: .ascii "hello world\0" #类似于C语言中 char* msg="hello world"

.text
la $a0,msg
li $v0,4
syscall

数据定义

  1. 定义整型数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # 打印Integer数据
    .data
    age: .word 23 #一个字长数据32位

    .text
    li $v0,1
    lw $a0,age
    syscall


    #========================#
    #加法运算
    .data
    number1: .word 2
    number2: .word 5

    .text
    lw $t0,number1
    lw $t1,number2

    add $t2,$t0,$t1

    li $v0,1
    move $a0 $t2
    syscall

    # 乘法运算 使用mul
  2. 定义Float数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #打印float数据
    .data
    PI: .float 3.14

    .text
    li $v0,2
    lwc1 $f12,PI
    syscall

    #float数据 算数运算
    .data
    f1: .float 3.14
    f2: .float 3.15
    .text
    li $v0,2
    lwc1 $f1,f1
    lwc1 $f2,f2
    add.s $f3,$f2,$f1 #带.s后缀的指令皆为浮点单精度指令
    mov.s $f12,$f3
    syscall
  3. 定义Double数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #打印double数据
    .data
    ayDouble: .double 7.20
    zeroDouble: .double 0.0
    .text

    ldc1 $f2,ayDouble
    ldc1 $f0,zeroDouble
    li $v0,3
    add.d $f12,$f2,$f0 #带.d后缀的指令皆为浮点双精度指令
    syscall
  4. 定义字符串数据

    1
    2
    3
    4
    5
    6
    7
    #打印double数据
    .data
    msg: .ascii "hello world"
    .text
    li $v0,4
    la $a0,msg
    syscall

用户输入

  1. 字符串输入

    1
    2
    3
    4
    5
    6
    7
    .data
    userInput: .space 20 #声明一块空间 默认存放0
    .text
    li $v0,8
    la $a0,userInput #将用户的输入存放至userInput中
    li $a1,20 #限制用户输入, 一旦超过20个默认回车
    syscall
  2. 整型数据输入

    1
    2
    3
    4
    # 输入的结果系统会存放在$v0寄存器
    .text
    li $v0,5
    syscall
  1. 浮点型数据输入

    1
    2
    3
    4
    #以float为例  输入的结果会存放在$f0寄存器
    .text
    li $v0,6
    syscall

单精度和双精度

单精度数(float型)在32位计算机中存储占用4字节,也就是32位,有效位数为7位,小数点后6位。

双精度数(double型)在32位计算机中存储占用8字节,也就是64位,有效位数为16位,小数点后15位。

浮点寄存器

在mips中一共有32个浮点寄存器(其中包含16个双精度浮点寄存器),用于单独处理浮点数

函数声明和调用

  1. 函数声明

    • 格式

      1
      2
      3
      函数名:
      函数体
      jr $ra #$ra寄存器中保存着调用指令下一条代码所在的地址
    • 示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      .data

      .text
      show:
      li $v0,1
      li $a0,3
      syscall

      jr $ra ;jump registers
  1. 函数调用

    • 格式

      1
      jal 函数名
    • 示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      .data

      .text
      jal show #调用函数时将下一条指令的地址存放至$ra寄存器

      #结束程序
      li $v0,10
      syscall

      show:
      li $v0,1
      li $a0,3
      syscall

      jr $ra
  2. 函数传参和返回值

    在mips汇编中,函数的参数一般放在$a系列寄存器当中,最多对应4个参数,超过4个部分使用栈储存. 函数的变量和返回值一般放在$v系列寄存器当中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #需求:定义加法函数 并调用获取返回值int sum(int v,int b)
    main:
    addi $a1,$zero,50
    addi $a2,$zero,100

    jal add

    li $v0,1
    move $a0,$v1
    syscall

    #结束程序
    li $v0,10
    syscall

    add:
    add $v1,$a1,$a2
    jr $ra
  3. 嵌套函数

    由于每执行jal调用一次函数, 就会刷新$ra寄存器中的值,因此,在嵌套函数调用之前,需要临时保存上一次$ra中的值,使用栈空间临时保护即可

栈操作

  1. 栈空间拉伸和平衡

    1
    2
    3
    addi $sp,$sp,-4 #栈拉伸 拉伸4个字节空间

    addi $sp,$sp,4 #栈平衡
  2. 入栈和出栈

    1
    2
    3
    sw $s0 ,0($sp) #入栈   往内存中写入数据

    lw $s0, 0($sp) #出栈 从内存中读取数据
  3. 嵌套函数使用栈保护$ra代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    main:
    addi $a1,$zero,50
    addi $a2,$zero,100

    jal add

    li $v0,1
    move $a0,$v1
    syscall

    #结束程序
    li $v0,10
    syscall

    add:
    addi $sp,$sp,-4 #栈拉伸
    sw $ra ,0($sp) #入栈

    jal sub
    add $v1,$a1,$v1

    lw $ra, 0($sp) #出栈
    addi $sp,$sp,4 #栈平衡
    jr $ra

    sub:
    sub $v1,$a1,$a2
    jr $ra

内存空间布局

从mars中可以查看到内存分布起始物理地址

转成图后:

栈的伸缩在mips和x86架构中是由高地址往低地址进行伸缩, 在arm架构中可升序也可降序

内存碎片

在内存动态分配(heap区)过程中容易出现一些小且不连续的空闲内存区域,这些未被使用的内存称作内存碎片

分类:

  • 内部碎片:比如数据在内存中采用4个字节对齐的方式进行存储, 比如我们申请一块3个字节的空间用于存储一个数据,但是系统给我们分配了4个字节空间,这时多出来的一个字节的空间就被称之为内部碎片
  • 外部碎片:在我们进行内存回收和分配的时候容易出现外部碎片,比如我连续申请了三块4个字节的内存空间,当我释放第二块内存空间然后紧接着申请一块8个字节的空间,此时由于之前释放的4个字节空间太小无法使用,这就造成了内存块空闲,这种碎片叫做外部碎片

PC 寄存器

称作 程序计数寄存器(Program Counter Register) :用于存储程序即将要执行的指令所对应在内存中的实际物理地址, 如果改变该值可以让指令跳转到我们想要跳转的地方

如何修改pc寄存器中的值

使用以下转移指令

  1. jr指令

    1
    jr 寄存器  #$ra寄存器实际上就是保存着函数调用前的pc寄存器的值
  2. jal指令

    1
    jal 标号  #跳转的同时给$ra寄存器赋值
  3. j指令

    1
    j 标号   #直接跳转

内存数据的读写

  1. 从指定内存中读取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    #整型数据
    lw $t0,0x10010000 #读取4个字节数据 赋值给t0寄存器
    ld $t0,0x10010000 #读取8个字节数据 赋值给t0和t1寄存器

    #单精度浮点数据
    lwc1 $f0,0x10010000


    #双精度浮点数据
    ldc1 $f0,0x10010000


    #字符数据
    ### 由于字符数据是以ascii码16进制的形式存放到内存中,因此只能获取到ascii码值,该值属于整型数据,直接使用lw或者ld即可
    lw $t0,0x10010000 #读取4个字节数据 赋值给t0寄存器
    ld $t0,0x10010000 #读取8个字节数据 赋值给t0和t1寄存器

    从内存中读取数据的宽度取决于寄存器的大小,由于32位cpu寄存器最大存储32位数据,因此lw $t0表示一次性读取4个字节的数据到$t0寄存器, 如果想要连续读取八个字节的数据,那么需要使用ld $t0,表示一次性读取8个字节的数据到$t0,$t1连个连续的寄存器,

  2. 往指定内存中写入数据

    1. 第一种 数据定义的同时指定物理地址
    1
    2
    3
    4
    5
    6
    .data 0x10010020 ;将以下定义的数据存放在0x10010020这个物理地址
    .ascii "a"
    .ascii "b"

    .data 0x10010000
    .ascii "c"
    1. 第二种 在代码段中使用指令
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #整型数据
    li $s1,4
    sw $s1,0x10010000 ;将$s1寄存器中的数据存入0x10010000这个物理地址

    #单精度浮点数
    .data
    f1: .float 3.14

    .text
    lwc1 $f2,f1
    swc1 $f2,0x10010000


    #双精度浮点数
    .data
    d1: .double 3.14

    .text
    ldc1 $f2,d1
    sdc1 $f2,0x10010000

以上直接使用的是简单粗暴的十六进制表示物理地址,很多时候内存的地址会保存在寄存器中,你可能会看到以下写法:

1
2
3
4
5
6
7
8
9
10
11
lw $s1, $s2
sw $s1, $s2

或者
lw $s1, 20($s2)
sw $s1, 20($s2) ;将地址往高位偏移20个字节 相当于sw $s1, 20+$s2


或者
lw $s1, -20($s2)
sw $s1, -20($s2) ;将地址往低位偏移20个字节

注意: 往指定内存中读取写入数据时,代码段不允许直接写入和读取

一维数组的定义

数组本质上就是多个数据的集合,在内存中按照一定顺序排列,角标即为每个数据的偏移值,在mips中内存数据是按照4个字节进行对齐的,也就是说一个数据最少占用4个字节内存空间,因此数组中数据之间的偏移量固定为n*4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.data
array: .space 20 #别名的另外一种用法 通过array(寄存器)这种格式 寄存器中存放地址偏移地址量

.text
# $t0寄存器存放角标值 $s1中存放需要存入的值
li $s1,1
li $t0,0
sw $s1,array($t0) #相当于 sw $s1,array+$t0

li $s1,2
li $t0,4
sw $s1,array($t0)

li $s1,3
li $t0,8
sw $s1,array($t0)

数组的打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
.data
array: .space 20

.text
#初始化数组中的数据
li $s1,1
li $t0,0
sw $s1,array($t0)

li $s1,2
li $t0,4
sw $s1,array($t0)

li $s1,3
li $t0,8
sw $s1,array($t0)


#查找角标为2的数值
getData:
la $s1 ,array
li $a0,2
mul $a0,$a0,4
add $s1,$s1,$a0
lw $a0,0($s1)
li $v0,1
syscall

#将角标临时置为0 方便下面循环操作
li $t0,0
while:
beq $t0,12,exit
lw $t2,array($t0)

addi $t0,$t0,4

li $v0,1
move $a0,$t2
syscall
j while

exit:
li $v0,10
syscall
快速初始化数组数据的方法
1
2
.data
array: .word 20 :3 #批量定义3个整型数据20

分支跳转指令

  1. 整型数据分支比较跳转
  • bgt(branch if greater than):用于大于比较
1
2
3
4
bgt $t0,$t1,sub # 如果$t0中的数据大于$t1,则跳转到sub分支,执行sub中的代码,否则,按照顺序执行bgt下面的代码, sub是一个代号,可以自定义


sub:
  • beq(branch equal):用于等于比较
1
2
3
4
beq $t0,$t1,sub # 如果$t0中的数据等于$t1,则跳转到sub分支,执行sub中的代码,否则,按照顺序执行bgt下面的代码, sub是一个代号,可以自定义


sub:
  • ble(branch if less than):用于小于比较
1
2
3
4
ble $t0,$t1,sub # 如果$t0中的数据小于$t1,则跳转到sub分支,执行sub中的代码,否则,按照顺序执行bgt下面的代码, sub是一个代号,可以自定义


sub:

练习1: 将以下c代码转换成mips汇编代码:

1
2
3
4
5
6
7
8
scanf("%d",$a);
scanf("%d",$b);

if(a>b){
printf("YES");
}else{
printf("NO");
}

汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 用$t0指代a ,$t1指代b

.data
msg_yes: .ascii "YES\0" # \0表示字符串结尾
msg_no: .ascii "NO\0"


.text
li $v0,5 #控制syscall为读取integer状态
syscall # 此时io控制台显示光标,可输入数字,回车后将输入的数字保存在$v0中
move $t0,$v0 #由于接下来还需要使用$v0 ,为避免数据被覆盖掉 将输入的数据转移到$t0中进行临时保存

li $v0,5
syscall
move $t1,$v0


bgt $t0,$t1,sub
li $v0,4
la $a0,msg_no
syscall

#结束程序
li $v0,10
syscall
sub:
li $v0,4
la $a0,msg_yes
syscall

练习2: 将以下c代码转换成mips汇编代码:

1
2
3
4
5
6
7
8
9
10
11
//求累加之和
//1+2+3+.....+100

int i=1;
int s=0;

while(i<=100){
s=s+i;
i=i+1;
}
printf("%d",s);

汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 用$t0指代i ,$t1指代s

.text
li $t0 ,1
li $t1 ,0


loop:
# s=s+i;
add $t1,$t1,$t0
add $t0,$t0,1

ble $t0,100,loop


move $a0,$t1
li $v0,1
syscall
  1. 浮点型数据分支比较
  • 小于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .data
    num1: .float 3.14
    num2: .float 3.16

    .text

    lwc1 $f0,num1
    lwc1 $f1,num2

    c.lt.s $f0,$f1 #关键代码
    bc1t sub #bc1t表示条件满足 bc1f表示不满足条件


    sub:
  • 等于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .data
    num1: .float 3.14
    num2: .float 3.16

    .text

    lwc1 $f0,num1
    lwc1 $f1,num2

    c.eq.s $f0,$f1 #关键代码 c表示coproc协处理寄存器 s表示single单精度
    bc1t sub #bc1t表示条件满足 bc1f表示不满足条件


    sub:
  • 小于等于

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .data
    num1: .float 3.14
    num2: .float 3.16

    .text

    lwc1 $f0,num1
    lwc1 $f1,num2

    c.le.s $f0,$f1 #关键代码
    bc1t sub #bc1t表示条件满足 bc1f表示不满足条件


    sub:

    以上是单精度浮点数据的比较示例,如果是双精度,只需将结尾.s改成.d即可

mips多文件开发

在文件A中定义函数

1
2
3
4
5
fun:
li $v0,1
li $a0,1
syscall
jr $ra

在文件B中使用关键字.include引用A文件中的函数

1
2
3
4
.text
jal fun

.include "A.asm"

所有文件必须在同一目录下

宏

  1. 宏替换

    全局替换,使用我们之前学过的.include伪指令进行替换

  2. 宏匹配

在汇编中,如果我们要依次打印1,2,3三个整数,那么汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print1:
li $v0,1
li $a0,1
syscall
jr $ra

print2:
li $v0,1
li $a0,2
syscall
jr $ra

print2:
li $v0,1
li $a0,3
syscall
jr $ra

我们发现使用标签的方式定义函数,当函数体内容存在不确定变量值时,代码非常冗余, 如果使用高级语言进行封装的话,我们一般一个函数就搞定了:

1
2
3
void print(int a){
print(a);
}

有没有办法使得汇编能像高级语言一样简洁呢?

在MARS中给我们提供了一个扩展伪指令,叫做宏匹配

宏匹配使用的格式如下:

1
2
3
.macro 别名
#汇编指令...
.end_macro

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
li $v0,10
syscall

#比如我们要对以上两行指令使用宏匹配进行封装

#封装结果为
.macro exit
li $v0,10
syscall
.end_macro


#在代码中引用
.text
exit #直接使用别名调用

如果我们要封装一个打印整型数据的函数,那么我们可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
#封装结果为
.macro print_int(%param)
li $v0,1
li $a0,%param
syscall
.end_macro


#在代码中引用
.text
print_int(1) #直接使用别名调用
print_int(2)
print_int(3)

这样是不是和高级语言没什么区别啦

打印字符串封装示例:

1
2
3
4
5
6
7
8
9
10
11
.macro print_str (%str)
.data
myLabel: .asciiz %str
.text
li $v0, 4
la $a0, myLabel
syscall
.end_macro

print_str ("test1")
print_str ("test1")

然后结合我们之前学过的多文件开发,完全可以将这个封装好的函数单独放在一个文件中,直接在头部.include就行

  1. 宏定义

    全局定义,如果我们想给一个数据或者寄存器,甚至是一行代码取个别名,然后在代码中使用别名的方式指代,那么可以使用宏定义指令.eqv 别名的好处就是方便我们进行记忆

    1
    2
    3
    .eqv  LIMIT      20 #给20这个立即数取个别名为LIMIT
    .eqv CTR $t2
    .eqv CLEAR_CTR add CTR, $zero, 0

    当我们有以下代码:

    1
    2
    3
    4
    .text
    li $v0,1
    add $t2, $zero, 0
    li $t0,20

    如果我们使用宏定义,我们可以写成如下:

    1
    2
    3
    4
    5
    6
    7
    8
    .eqv  LIMIT      20 #给20这个立即数取个别名为LIMIT
    .eqv CTR $t2
    .eqv CLEAR_CTR add CTR, $zero, 0

    .text
    li $v0,1
    CLEAR_CTR
    li $t0,LIMIT

注:宏定义和宏匹配必须先定义后使用,也就是说定义的代码需要放在前头

二维数组的定义

二维数组其实就类似于我们数学中的二维坐标系,我们如果要定位一个点的话可以使用(x,y)来表示,在计算机的世界里,二维中所有的点都按照顺序依次存放在内存当中

假设我们将第一维当做行,第二维当做列,那么排列的方式有以下两种:

第一种是 行不动,列轮动

内存地址 二维坐标
0x00000020 arr[2][2]
0x0000001C arr[2][1]
0x00000018 arr[2][0]
0x00000014 arr[1][2]
0x00000010 arr[1][1]
0x0000000C arr[1][0]
0x00000008 arr[0][2]
0x00000004 arr[0][1]
0x00000000 arr[0][0]

这种方式获取实际地址的公式为:

1
2
3
4
5
6
addr=baseAddr+(rowIndex*colSize+colIndex)*dataSize

实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度

比如:我要计算arr[2][1]的实际物理地址, 那么
实际地址=0x00000000+(2*3+1)*4=0x00000000+0x0000001C=0x0000001C

第二种是 列不动,行轮动

内存地址 二维坐标
0x00000020 arr[2][2]
0x0000001C arr[1][2]
0x00000018 arr[0][2]
0x00000014 arr[2][1]
0x00000010 arr[1][1]
0x0000000C arr[0][1]
0x00000008 arr[2][0]
0x00000004 arr[1][0]
0x00000000 arr[0][0]

这种方式获取实际地址的公式为:

1
2
3
4
5
6
addr=baseAddr+(colIndex*rowSize+rowIndex)*dataSize

实际地址=首地址+(第几列*行数+第几行)*数据占用的宽度

比如:我要计算arr[2][1]的实际物理地址, 那么
实际地址=0x00000000+(1*3+2)*4=0x00000000+0x00000014=0x00000014

使用mips汇编实现二维数组定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


#以下是以 行不动 列轮动的方式实现
.data
da: .word 1,2,3,5,6,7,10,11,12
array: .space 36

.text
# void initArry(*arr,row,col,index)
initArry:
#首地址
la $a0,array
#第几行
li $a1,0
#第几列
li $a2,0
#存第几个数
li $a3,0

while:
# arr[x][y]
bgt $a1,2,exit
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-12
sw $a1,8($sp)
sw $a2,4($sp)
sw $a3,0($sp)

jal saveDataToMemory
#从栈中恢复局部变量值
lw $a1,8($sp)
lw $a2,4($sp)
lw $a3,0($sp)
add $sp,$sp,12

#累加计数
add $a3,$a3,4
#列轮动
addi $a2,$a2,1
bgt $a2,2,sub

j while

sub:
li $a2,0
add $a1,$a1,1
j while

saveDataToMemory:
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-4
sw $ra,0($sp)

#计算数据存放的物理地址
jal getAddr

#获取需要存放的数据
lw $t0,da($a3)
#将数据存入指定位置中
sw $t0,0($v0)

lw $ra,0($sp)
add $sp,$sp,4
jr $ra

#算法部分
getAddr:
#实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度
mul $a1,$a1,3
add $a2,$a2,$a1
mul $a2,$a2,4
add $v0,$a2,$a0

jr $ra


exit:
#程序结束之前测试数据能否正常取出
jal getDataFromArry
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出值为11
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

列不不动 行轮动方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


#以下是以 列不动,行轮动的方式实现
.data
da: .word 1,2,3,5,6,7,10,11,12
array: .space 36

.text
# void initArry(*arr,row,col,index)
initArry:
#首地址
la $a0,array
#第几行
li $a1,0
#第几列
li $a2,0
#存第几个数
li $a3,0

while:
# arr[x][y]
bgt $a2,2,exit
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-12
sw $a1,8($sp)
sw $a2,4($sp)
sw $a3,0($sp)

jal saveDataToMemory
#从栈中恢复局部变量值
lw $a1,8($sp)
lw $a2,4($sp)
lw $a3,0($sp)
add $sp,$sp,12

#累加计数
add $a3,$a3,4
#行轮动
addi $a1,$a1,1
bgt $a1,2,sub

j while

sub:
li $a1,0
add $a2,$a2,1
j while

saveDataToMemory:
# 避免寄存器中的参数被子函数覆盖 将数据放置在栈中临时保存
add $sp,$sp,-4
sw $ra,0($sp)

#计算数据存放的物理地址
jal getAddr

#获取需要存放的数据
lw $t0,da($a3)
#将数据存入指定位置中
sw $t0,0($v0)

lw $ra,0($sp)
add $sp,$sp,4
jr $ra

#算法部分
getAddr:
#实际地址=首地址+(第几列*行数+第几行)*数据占用的宽度
mul $a2,$a2,3
add $a1,$a1,$a2
mul $a1,$a1,4
add $v0,$a1,$a0

jr $ra


exit:
#程序结束之前测试数据能否正常取出
jal getDataFromArry
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出7
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

更为简便的方法实现二维数组的搭建

由于数组中数据是在内存中连续进行排列存储的,那么我们可以之间将数据 依次存入内存之中,然后使用算法进行数据获取即可(以下示例皆采用 行不动,列动 的方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


#由于数组中的数据是存放在堆内存中,需要在程序执行时动态分配


.text
#堆地址从0x10040000开始
la $a0,0x10040000
#将数据按照顺序存放至内存中
jal initData

#获取数据
jal getDataFromArry
j exit

initData:
li $s1,1
sw $s1,0($a0)

li $s1,2
sw $s1,4($a0)

li $s1,3
sw $s1,8($a0)

li $s1,5
sw $s1,12($a0)

li $s1,6
sw $s1,16($a0)

li $s1,7
sw $s1,20($a0)

li $s1,10
sw $s1,24($a0)

li $s1,11
sw $s1,28($a0)

li $s1,12
sw $s1,32($a0)


jr $ra


#算法部分
getAddr:
#实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度
mul $a1,$a1,3
add $a2,$a2,$a1
mul $a2,$a2,4
add $v0,$a2,$a0

jr $ra


exit:
#程序退出
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出值为11
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

再简化一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#需求:实现int a[3][3] = {{1, 2, 3}, {5, 6, 7}, { 10, 11, 12}};


.data
da: .word 1,2,3,5,6,7,10,11,12
# 假如以上数据是动态写入的 当做数组中的数据来用

.text
#那么只需要提供首地址然后配合算法就能获取到指定坐标的数据
la $a0,da
jal getDataFromArry
j exit


#算法部分
getAddr:
#实际地址=首地址+(第几行*总列数+第几列)*数据占用的宽度
mul $a1,$a1,3
add $a2,$a2,$a1
mul $a2,$a2,4
add $v0,$a2,$a0

jr $ra


exit:
#程序退出
li $v0,10
syscall




#获取指定坐标位置的数据arr[2][1] 输出值为11
getDataFromArry:
#第几行
li $a1,2
#第几列
li $a2,1
#计算物理地址
jal getAddr
#获取数据
lw $t0,0($v0)

#打印数据
move $a0,$t0
li $v0,1
syscall

按照正常的编程思维,我们一般使用第一种行不动 列动的存储方式 第一维为行,第二维为列,如果你想改成行动存储方式,有两种方法:要么将数据的存储顺序进行变动,配合第二种算法,要么将第二维当成行,第一维当成列,配合第二种算法进行处理

Mips汇编指令汇总表

类别

指令名称

实例

含义

注释

英文注解

算

数

加法

add $s1,  $s2,  $s3

$s1 = $s2 + $s3

三个寄存器操作数

addition 加法

减法

sub $s1,  $s2,  $s3

$s1 = $s2 - $s3

三个寄存器操作数

subtraction 减法

立即数加法

addi $s1, $s2,  20

$s1 = $s2 + 20

用于加常数数据

add immediate 立即加法

数

据

传

输

取字

lw $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将一个字从内存中取到寄存器中

load word 加载字

存字

sw $s1, 20 ($s2)

Memory[$s2 + 20] = $s1

将一个字从寄存器中取到内存中

store word 存储字

取半字

lh $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将半个字从内存中取到寄存器中

load halfword 加载半字

取无符号半字

lhu $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将半个字从内存中取到寄存器中

load halfword unsigned

存半字

sh $s1, 20 ($s2)

Memory[$s2 + 20] = $s1

将半个字从寄存器中取到内存中

stroe halfword 存储半字

取字节

lb $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将一字节从内存中取到寄存器中

load byte

取无符号字节

lbu $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

将一字节从内存中取到寄存器中

load byte unsigned

存字节

sb $s1, 20 ($s2)

Memory[$s2 + 20] = $s1

将一字节从寄存器中取到内存中

store byte

取链接字

ll $s1, 20 ($s2)

$s1 = Memory[$s2 + 20]

取字作为原子交换的前半部

load linked

存条件字

sc $s1, 20 ($s2)

Memory[$s2 + 20] = $s1;

$s1 = 0 or 1

存字作为原子交换的后半部分

store conditional

取立即数的高位

lui $s1, 20

$s1 = 20 * 216

取立即数并放到高16位

load upper immediate

逻

辑

与

and $s1, $s2, $s3

$s1 = $s2 & $s3

三个寄存器操作数按位与

and

或

or $s1, $s2, $s3

$s1 = $s2 | $s3

三个寄存器操作数按位或

or

或非

nor $s1, $s2, $s3

$s1 = ~ ($s2 | $s3)

三个寄存器操作数按位或非

not or

立即数与

andi $s1, $s2, 20

$s1 = $s2 & 20

和常数按位与

and immediate

立即数或

ori $s1, $s2, 20

$s1 = $s2 | 20

和常数按位或

or immediate

逻辑左移

sll $s1, $s2, 10

$s1 = $s2 << 20

根据常数左移相应位

set left logical

逻辑右移

srl $s1, $s2, 10

$s1 = $s2 >> 20

根据常数右移相应位

set right logical

条

件

分

支

相等时跳转

beq $s1, $s2, 25

if ($s1 == $s2) go to 

PC + 4 + 25 * 4

相等检测:

和PC相关的跳转

branch on equal

不相等时跳转

bne $s1, $s2, 25

if ($s1 != $s2) go to 

PC + 4 + 25 * 4

不相等检测:

和PC相关的跳转

branch on not equal

小于时跳转

slt $1, $s2, $3

if ($s2 < $s3) $s1 = 1; 

else $s1 = 0

比较是否小于

set less than

无符号数比较小时置位

sltu $1, $s2, $3

if ($s2 < $s3) $s1 = 1; 

else $s1 = 0

比较是否小于无符号数

set less than unsigned

无符号数小于立即数时置位

slti $1, $s2, 20

if ($s2 < 20) $s1 = 1; 

else $s1 = 0

比较是否小于常数

set less than immediate

无符号数比较小于无符号立即数时置位

sltiu $1, $s2, 20

if ($s2 < 20) $s1 = 1; 

else $s1 = 0

比较是否小于无符号常数

set less than immediate unsigned

无

条

件

跳

转

跳转

j 2500

go to 2500 * 4

跳转到目标地址

jump

跳转至寄存器所指位置

jr $ra

go to $ra

用于switch语句,以及过程调用

jump register

跳转并链接

jal 2500

$ra = PC + 4;

go to 2500 * 4;

用于过程调用(方法)

正常的执行流程执行完A自然要执行B指令,现在要跳转执行C方法,这时就把B地址存入寄存器中,执行完C后跳转到B

jump and link

Mips内存结构图:

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

1…282930…50

乱码三千

android程序员一枚,擅长java,kotlin,python,金融投资,欢迎交流~

491 日志
143 标签
RSS
© 2025 乱码三千
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%