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

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


  • 首页

  • 归档

  • 搜索

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!

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

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

各个平台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/。

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

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

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内存结构图:

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

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

汇编语言之ARM64汇编

发表于 2020-12-14

寄存器

CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。

CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器。

对于arm64系的CPU来说, 如果寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的。

高速缓存

iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M.

CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成).CPU直接从高速缓存依次读取指令来执行.

通用寄存器

  • ARM64拥有有31个64位的通用寄存器 x0 到 x30,这些寄存器通常用来存放一般性的数据,称为通用寄存器(有时也有特定用途)
    • 那么w0 到 w28 这些是32位的. 因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
    • 比如 w0 就是 x0的低32位!

数据地址寄存器

数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
ARM64中

  • 64位: X0-X30, XZR(零寄存器 ,里面存放数据0)
  • 32位: W0-W30, WZR(零寄存器)

注意:
有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中.在ARM中并没有

浮点和向量寄存器

因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数

  • 浮点寄存器 64位: D0 - D31 32位: S0 - S31

现在的CPU支持向量运算.(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

  • 向量寄存器 128位:V0-V31

PC寄存器(program counter)

为指令指针寄存器, 它指示了CPU当前要读取指令的地址, 类似于x86汇编种的cs+ip

SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址.
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!()

注意:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp
ARM64里面 对栈的操作是16字节对齐的!!

关于内存读写指令

注意:读/写 数据是都是往高地址读/写 也就是sp指针是从高地址往低地址移动但是指向的数据是往高地址读写,堆指针是从低往高地址移动,堆和栈各占一头,两个指针相撞则抛出堆栈内存溢出

str(store register)指令

将数据从寄存器中读出来,存到内存中.

ldr(load register)指令

将数据从内存中读出来,存到寄存器中

此ldr 和 str 的变种ldp(pair) 和 stp(pair) 还可以操作2个寄存器.

1
2
3
4
5
6
7
8
9
;利用栈进行数据交换
.text
.global _A
_A:
sub sp,sp,#0x20 ;sp=sp-0x20 开辟一个32字节的占空间
stp x0,x1,[sp,#0x10] ;然后将x0和x1中的数据存入sp+0x10所指向的栈空间 []相当于是获取指定地址的空间,不会改变sp原来的值,如果想改变sp的值只需在末尾加上! 也就是[sp,#0x10]! 即可
ldp x1,x0,[sp,#0x10] ;读取sp+0x10这块栈空间中的数据存放至x1,x0寄存器中
add sp,sp,#0x20 ;栈平衡, 释放内存空间
ret ;返回至调用指令下一行

stur指令: 偏移量为减时使用 . stur w0, [x29, #0x8] 偏移量为负的 将寄存器w0的值存入x29 - 0x8 的内存地址

[sp]: sp保存栈空间的地址值, [sp]表示取值,获取所对应的空间 和8086中的[bx]是类似的

另外 汇编简写

1
2
3
4
5
6
7
8
9
10
11
12
stp x29,x30,[sp,#-0x10]! ;尾部多了一个!号

;相当于一下两行代码
sub sp,sp,#0x10
stp x29,x30,[sp]
;或者直接理解为加了!号的sp值会发生改变


ldp x29,x30 ,[sp],#x010
;相当于以下两行代码
ldp x29,x30,[sp]
add sp,#0x10

bl指令

  • CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令
  • ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如
    • mov x0,#10、mov x1,#20
  • 但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能
  • ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的是bl指令

类似于x86汇编中的, call

bl标号

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处执行指令

ret

  • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

ARM64平台的特色指令,它面向硬件做了优化处理的

x30寄存器

x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!

注意:在函数嵌套调用之前的时候.需要将x30入栈!

arm代码示例

1
2
3
4
5
6
7
8
9
10
11
12
.text ;代码段
.global _A,_B ;定义两个全局函数 A和B

_A:
mov x0 ,#0xa0 ;arm汇编中数据用#开头
mov x1 ,#0x00
add x1 ,x0,#0x14 ;x1=x0+0x14
ret ;返回到bl指令所对应的下一条指令

_B:
add x0, x0,#0x10
ret

寄存器和栈

寄存器是全局容器,所有函数共用,但是栈不一样,一个函数占用独有的栈空间, 在各个函数嵌套调用时,寄存器很容易被覆盖读写,这个时候为了保持寄存器的数据不被改变,通常结合栈临时保存寄存器中的值,然后函数ret之前将数据恢复,这样就能确保上一个函数的数据不被改变,也就是实现了将寄存器当做局部变量使用

栈的对齐

ARM64里面 对栈的操作是16字节对齐的, 也就是一次开辟栈空间至少是16字节, 或者是16的倍数, 如果不是这个值会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text ;代码段
.global _A,_B ;定义两个全局函数 A和B

_A:
mov x0 ,#0xaaaa ;arm汇编中数据用#开头
str x30,[sp,#-0x10]! ;在调用下一个函数之前临时保存lr寄存器中的地址, lr保存bl _A的下一条指令地址
bl _B
mov x0,#0xcccc
ldr x30,[sp],#0x10 ;lr恢复
ret ;返回到bl指令所对应的下一条指令

_B:
add x0, x0,#0x10
ret

既然sp一次最少拉伸16个字节, 那么以下函数需要拉伸多少空间:

1
2
3
4
5
6
void sum(int a, int b){
int c=3;
int c=4;
int c=5;

}

由于int类型的数据占用4个字节空间, 这里一共有5个int,那么需要占用5*4=20个字节的空间, 那么sp一次性拉伸0x20也就是32字节的栈空间

1
2
3
16位寄存器-->最大装2个字节数据-->0xFFFF
32位寄存器-->最大装4个字节数据-->0xFFFFFFFF
64位寄存器-->最大装8个字节数据-->0XFFFFFFFFFFFFFFFF

如果函数里面又调用了函数,那么sp拉伸多少呢?

1
2
3
4
5
6
void sum(int a, int b){
int c=3;
int c=4;
int c=sumb(a,b);

}

由于bl调用函数之前会复写x30(lr)寄存器中的值, 所以需要将x29和x30寄存器进行临时保护, 这两个寄存器占用16个字节, 加上sum函数的局部变量和参数所占的16个字节,一共是32个字节

叶子函数

函数体中没有调用其他函数的函数称之为叶子函数,又称为末尾函数

这种函数在编写汇编代码时可以省略使用栈空间, 栈空间是为了临时保护数据不被下一个函数污染, 叶子函数不存在这种风险,所以不需要进行保护处理,直接使用寄存器即可

ARM64方法返回值

ARM64下,函数的参数通常情况下是存放在X0到X7(W0到W7)这8个寄存器里面的.如果超过8个参数,就会入栈.(一是跟参数个数有关,另外还更数据结构有关,指针占用8个字节刚好一个64位寄存器, 如果仓鼠类型超出8个字节,即存放到其他地方,比如栈空间)
函数的返回值通常情况下是放在X0 寄存器里面的.

orr指令

称为或指令, 进行或运算, https://blog.csdn.net/qq_39416311/article/details/102762635

1
orr w8,wzr,#0x1  ;将立即数0x1和0进行或运算, 然后复制给w8

函数嵌套复用

假如有两个函数A和B,它们的调用链为:A–>B–>A

在高级语言中,A函数进行了复用,但是在汇编当中并没有复用的概念,每调用一个函数便开辟一次栈空间, 因此哪怕是调用同一个函数,如果递归嵌套次数过多,就会造成内存溢出

状态寄存器(标记寄存器)

cpsr(current program status registers)寄存器

CPSR和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义.而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息.

要想在算数运算是影响标记寄存器的值,必须在指令后面加上s,比如:

1
2
add--->adds
sub--->subs

注:CPSR寄存器是32位的

  • CPSR的低8位(包括I、F、T和M[4:0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位!
  • N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行!意义重大!

N(Negative)标志

CPSR的第31位是 N,符号标志位。它记录相关指令执行后,其结果是否为负.如果为负 N = 1,如果是非负数 N = 0.

   注意,在ARM64的指令集中,有的指令的执行时影响状态寄存器的,比如add\sub\or等,他们大都是运算指令(进行逻辑或算数运算);

Z(Zero)标志

CPSR的第30位是Z,0标志位。它记录相关指令执行后,其结果是否为0.如果结果为0.那么Z = 1.如果结果不为0,那么Z = 0.

   对于Z的值,我们可以这样来看,Z标记相关指令的计算结果是否为0,如果为0,则N要记录下”是0”这样的肯定信息.在计算机中1表示逻辑真,表示肯定.所以当结果为0的时候Z = 1,表示”结果是0”.如果结果不为0,则Z要记录下”不是0”这样的否定信息.在计算机中0表示逻辑假,表示否定,所以当结果不为0的时候Z = 0,表示”结果不为0”。

C(Carry)标志

CPSR的第29位是C,进位标志位。一般情况下,进行无符号数的运算。
加法运算:当运算结果产生了进位时(无符号数溢出),C=1,否则C=0。
减法运算(包括CMP):当运算时产生了借位时(无符号数溢出),C=0,否则C=1。

   对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N - 1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。如下图所示:

进位

   我们知道,当两个数据相加的时候,有可能产生从最高有效位想更高位的进位。比如两个32位数据:0xaaaaaaaa + 0xaaaaaaaa,将产生进位。由于这个进位值在32位中无法保存,我们就只是简单的说这个进位值丢失了。其实CPU在运算的时候,并不丢弃这个进位制,而是记录在一个特殊的寄存器的某一位上。ARM下就用C位来记录这个进位值。比如,下面的指令

1
2
3
4
5
mov w0,#0xaaaaaaaa;0xa 的二进制是 1010
adds w0,w0,w0; 执行后 相当于 1010 << 1 进位1(无符号溢出) 所以C标记 为 1
adds w0,w0,w0; 执行后 相当于 0101 << 1 进位0(无符号没溢出) 所以C标记 为 0
adds w0,w0,w0; 重复上面操作
adds w0,w0,w0

借位

   当两个数据做减法的时候,有可能向更高位借位。再比如,两个32位数据:0x00000000 - 0x000000ff,将产生借位,借位后,相当于计算0x100000000 - 0x000000ff。得到0xffffff01 这个值。由于借了一位,所以C位 用来标记借位。C = 0.比如下面指令:

1
2
3
4
mov w0,#0x0
subs w0,w0,#0xff ;
subs w0,w0,#0xff
subs w0,w0,#0xff

V(Overflow)溢出标志

CPSR的第28位是V,溢出标志位。在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出。

  • 正数 + 正数 为负数 溢出
  • 负数 + 负数 为正数 溢出
  • 正数 + 负数 不可能溢出

adrp指令

adrp(address page):地址页,用于计算指定数据所在物理地址和当前pc地址之间的偏移量, 也就是说通过该指令计算出常量的物理地址

1
2
3
4
5
6
7
adrp x0,1
;1.将1的值左移12位, 1 0000 0000 0000 ==0x1000
;2.将pc寄存器的低12位清零
;3.清零之后的值加上0x1000 然后将最后结果赋值给x0寄存器


;adrp指令后边的数值1为十六进制

内存分区

  • 代码区:可读可写可执行
  • 栈区: 可读可写
  • 堆区:动态申请, 可读可写
  • 全局变量区:可读可写
  • 常量区:只读

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

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

Android系统目前支持CPU架构都有哪些

发表于 2020-12-14

7种CPU架构

  • armeabi (ARM v5):32位cpu 属于 第5代、第6代早期的ARM处理器
  • armeabi-v7a (ARM v7):32位cpu 属于 第7代的 ARM 处理器 从2010年起
  • arm64-v8a (ARM v8): 第8代、64位ARM处理器
  • x86 : 32位处理器 从2011年起
  • x86_64 : 64位处理器 从2014年起
  • MIPS : 32位处理器 从2012年起
  • MIPS64 : 64位处理器 从2014年起

兼容和文件读取顺序

arm64-v8a是可以向下兼容的,其下有armeabi-v7a,armeabi
armeabi-v7a向下兼容armeabi

对于一个cpu是arm64-v8a架构的手机,它运行app时,进入jnilibs去读取库文件时,先看有没有arm64-v8a文件夹:

如果没有该文件夹,去找armeabi-v7a文件夹,如果没有,再去找armeabi文件夹,如果连这个文件夹也没有,就抛出异常
如果有arm64-v8a文件夹,那么就去找特定名称的.so文件,注意:如果没有找到,不会再往下(armeabi-v7a文件夹)找了,而是直接抛出异常

由于向下兼容的特性 高版本的设备可以使用低版本armeabi的so库, 但是低版本不支持高版本库, 这也就是为什么很多开发商包括微信只保留了armeabi的so库,从而兼容市面上所有的设备

只保留armeabi存在的问题

所有的x86/x86_64/armeabi-v7a/arm64-v8a设备都支持armeabi架构的.so文件,因此似乎移除其他ABIs的.so文件是一个减少APK大小的好技巧。但事实上并不是:这不只影响到函数库的性能和兼容性

64位设备(arm64-v8a, x86_64, mips64)能够运行32位的函数库,但是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)

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

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

1…272829…48

乱码三千

android程序员一枚,擅长java,kotlin,python,金融投资,欢迎交流~

479 日志
139 标签
RSS
© 2024 乱码三千
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%