关于字节
一个字节是固定为8个二进制位
关于字(word)
字的长度是可变的,它和cpu一次性能操作的实际位数有关
比如16位的cpu一次能操作16位数,因次2个字节代表一个字
如果是32位cpu,那么32位也就是4个字节代表一个字的长度(word)
关于半字
半字即为字长的一半,在16位cpu中,半字即为一个字节,故在16位中没有半字这个说法,一半32位以上cpu环境中常用
本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 码出一个新世界
ARM汇编开发指用ARM提供的汇编指令,进行ARM程序的开发。
ARM汇编开发,有两种开发方式,一种是使用ARM汇编,一种是使用ARM GNU汇编。两种汇编开发,使用的汇编指令是完全一样的,区别是宏指令,伪指令,伪操作不一样。其实两种开发方式的区别在于所使用的编译工具不一样。
对于ARM汇编,使用的是ARM公司开发的编译器,而ARM GNU汇编,是使用GNU为ARM指令集开发的编译器,也就是arm-gcc。
两种常用的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 name, {,attr,} … |
其中,name是程序段名, atrr是段名属性
对于属性,有以下一些:
指定对齐
ALIGN 4 表示4字节地址对齐
ALIGN 8 表示8字节地址对齐
注意:在AREA中使用和单独使用ALIGN的区别,在于格式和对齐的计算不一样。
指定汇编程序的入口。
一个程序至少有一个入口点,也可以有多个入口点,但是在一个源文件中,最多只能有一个ENTRY。当多个源文件均有ENTRY时,由链接器指定程序真正的入口。
表示源程序的结束
所以汇编语言源文件必须以END结束,汇编器遇到END, 将结束编译。
格式: EXPORT 标号 [,WEAK]
声明一个全局标号,其他源文件可以使用这个标号。WEAK表示碰上其他同名标号时,其他标号优先。
格式: IMPORT 标号,[,WEAK]
表示该引用的标号在其他源文件中,单要在当前文件中引用。WEAK表示找不到该标号时,也不报错,一般该标号置为0,如果是B 或BL指令用到该标号,该指令置为nop。
该标号会加入到当前源文件的符号表中。
和IMPORT一样,不同在于,如果当前文件没有引用该标号,该标号不会加入到当前源文件的符号表中。
将一个源文件含到当前的源文件中
对一个常量标号赋值
格式:
1 | name EQU expression |
其中: name符号名, expression寄存器相关或者程序相关的固定值
如:
1 | num EQU 2 ; 为符号赋予数字2 |
EQU,等同于C语言中用#define定义一个常量
用于分配一片连续内存单元,并用0初始化。SPACE可用%代替。
格式:
1 | {label} SPACE expr |
label : 是一个标号, 可选
expr: 分配的内存字节数
如
1 | stack SPACE 100 ; 分配100个字节内存单元,并用0初始化。标号stack是这片空间的起始地址 |
用于分配段字节内存单元,并用伪操作中的expr初始化。
格式:
1 | {label} DCB expr {,expr} |
label: 是一个标号,可选
expr: 可以是-128~255的数值或者字符串
如:
1 | string DCB "HELLO" ;为HELLO字符串分配空间, string是这块空间的起始地址 |
用于分配段字内存单元(分配的内存都是字对齐,DCDU并不严格字对齐),并用伪操作中的expr初始化。 DCD 可用 & 代替。
格式:
1 | {label} DCD expr, {,expr} |
label: 是一个标号,可选,表示这块内存单元的首地址
expr: 数字表达式或程序中的标号
如:
1 | data DCD 1,2,3,4 ;分配字对齐的字单元空间,初始化为1,2,3,4 |
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 | 在汇编时,被替换成空操作 |
| 伪操作 | 语法格式 | 作用 |
|---|---|---|
| .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 |
本文为作者原创 转载时请注明出处 谢谢

两种常用的ARM的编译开发环境
以上两种编译环境,使用的指令集都是一致的, 只是语法格式有不同,也就是宏指令,伪指令,伪操作不一样
使用 Keil μVision5 这款软件进行ARM32的汇编学习
下载地址:http://www.mcuzone.com/down/Software.asp?ID=10000503
| 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 | AREA test, CODE ;声明一个代码段,段的名称为test(名称可自定义),CODE关键字大小写都可,为了区分,一般大写 |
数据表示
1 | mov R0,#13 ;将十进制13赋值给R0寄存器 |
函数声明和调用
1 | AREA test, CODE |
声明一个数据段
1 | AREA da, DATA ;声明一个数据段 默认可读可写状态 |
数据定义
1 | # DCB |
字符串必须使用DCB进行定义
分配一块连续的内存空间
1 | sp1 SPACE 100 ;分配一块连续100个字节的空间 |
从内存中读取数据
使用中括号表示通过地址取值
1 | LDR R0,[R1] ;将内存地址为R1的字数据读入寄存器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为地址的存 储器中。
这个指令和内存读取指令长的一模一样,如果我们在使用的时候加个等号,那么它就是另外一个指令
1 | ;如果不加等号 是内存读取功能 |
实际上,加了等号的LDR指令,刚好可以弥补mov指令的不足, mov指令只能传送由八个二进制位右移而得的数据,而LDR则没有这个限制
也就是说如果我们想将一个数值传入寄存器,可以有两种方式:
1 | ;第一种 |
作用:
那么除了通过LDR伪指令来获取数据所在地址外还有一个指令也可以获取数据地址,那就是adr指令,但这个指令只能获取当前段内数据的地址,段外数据无法获取,ldr则没有这个限制
1 | AREA test2,CODE |
段属性拓展
READONLY
该段内存区域数据只能读取,不能写入,也就是如果使用内存读写指令,数据也写入不了
READWRITE
该段内存区域可读可写,不仅可以使用内存读写指令,还可以在调试的时候直接在memory窗口双击修改
示例:
1 | AREA code, CODE,READWRITE ;将代码段内存区域设置为可读可写状态,如果不写默认为只读 |
段对齐属性 ALIGN
1 | AREA code, DATA,ALIGN=3 ;对齐数值范围为0~31 |
该属性会使得该段的基地址进行相应的偏移,ALIGN=3表示基地址会在上一个段数据的基础之上偏移2^3=8个字节的位置
1 | AREA test1,DATA ;假设这个段的基地址为0x00000100 |
我们可以简单理解为,使用ALIGN这个属性可以让我们给上一个段预留除一部分缓冲区域,以ALIGN=2为例,当上一个段中的数据超过4个字节时,当前段基地器会向后再偏移4个字节,避免数据被覆盖,也就是说内存数据位置会进行重新分布,那么我们可以通过这个值来设置内存数据刷新频率,值越低,内存利用率越高,但是内存刷新频率也越高,负荷加重,反之,内存浪费越大,但是内存数据不需要频繁重新分布
另外:
除了在段属性中可以设置对齐之外,在指令中也可以插入ALGIN关键字:
1 | AREA code,CODE |
代码中使用AGLIN时用空格代替等号,同时单位为字节
多个代码段入口区分
当我们在同一个源文件中定义两个代码段时,程序从哪个段当做执行入口呢?
这个时候我们需要指定程序的入口,使用伪指令entry
1 | AREA test2,CODE |
1 | push {R0} ;将R0中的值存入栈内存中 相当于是STR R0,[R13,#-4] |
1 | pop {R0} ;将栈顶中的值取出存入R0寄存器 相当于是LDR R0,[R13],#0x0004 |
pop和push 本质上使用的是LDR和STR内存读写指令
如果想批量操作多个连续栈空间的话,直接使用逗号分隔开,连续标号的寄存器使用横杠分隔
1 | push {R0,R4-R12,LR} ;大括号中寄存器从右往左LR R12...R4 R0依次存入 |
除了使用pop和push之外,可以使用STM(store much)和LDM(load much)指令
格式:
1 | STM 起始地址/基地址寄存器,{寄存器名称,多个寄存器以逗号或者-分割} ;起始地址寄存器R0-R14任意选择 |
示例:
1 | STMFD R13!,{R0,R4-R12,LR} ;将寄存器列表中的寄存器(R0,R4到 R12,LR)存入栈,。 |
pop和push,它们内部也是转成STM和LDM指令:
相关后缀含义:
我们在使用的时候,要么使用结合的形式比如STMDB+LDMIA,要么直接使用封装形式比如STMFD+LDMFD
栈的生长方式可以有四种: 满增栈、满减栈、空增栈、空减栈
X86和mips架构都是采用满递减堆栈方式处理栈空间,ARM架构四种方式均支持
内存批量读写示例:
1 | mov R1,#4 |
如果我想在上面的基础上再往后追加数据
1 | mov R1,#6 |
我们发现数据并没有追加,而是被覆盖了,因为R0的值依然还是 0x00000008,这个时候我们需要使用扩展指令,如下:
1 | mov R1,#4 |
事实上 STM指令如果不加后缀写法,默认使用的是STMIA指令,LDM指令默认使用LDMIA
不管使用哪种扩展指令,皆为左低右高的形式,也就是左边的寄存器数据存放在低地址,右边的存放在高地址
1 | STMIA R0!,{R1,R2} ;左边R1的内容放低地址,右边R2的内容放高地址 |
宏匹配
1 | MACRO |
1 | MACRO |
第一个$label是干嘛用的呢,由于宏的内部处理方式的替换,为了避免标签名称的冲突,增加一个标识
1 | ;假如我要在宏匹配中定义一个函数fun, 当我调用两次的时候,会出现函数名重复的问题 |
宏定义
1 | ;全局数字变量 |
1 | ;局部数字变量 |
1 | num EQU 10 ;关键字EQU 数据不允许修改 |
宏定义示例
1 | ;定义一个全局整型数据宏 |
局部宏数据
1 |
|
常量数据
1 | AREA data ,DATA |
宏匹配和宏定义需要遵循先定义后使用的原则
如果需要从宏中跳出,可以使用伪指令MEXIT
宏替换
使用include或者get伪指令
1 | ;使用get伪指令 |
正常传送指令mov
1 | mov R0,#4 ;将4传送至R0寄存器 |
取反传送指令mvn,也叫数据非传送指令
1 | mvn R0,#4 ;将4取反后传送至R0寄存器 0100取反为1011 |
直接跳转,仅更改PC寄存器的值
示例:
1 | B 0x00040000 ;直接跳转到物理地址0x00040000读取指令并执行 |
跳转并链接,除了更改PC寄存器的值之外,还会将下一条指令所对应的物理地址存放至lr寄存器中
示例:
1 | BL 0x00040000 |
BX指令
跳转并切换状态
BX指令后面只能跟寄存器,弥补了B指令和BL指令的不足, 同时会根据寄存器中最低比特位值切换ARM/Thumb模式
示例:
1 | BL print |
除了通过指令来更改PC寄存器值之外,在ARM32中还可以直接使用传送指令对PC寄存器进行赋值:
1 | mov pc,#0x00000008 ;往pc寄存器中写入一个地址值 |
BLX指令
该指令将以下功能集于一身
算术运算
1 | add R0,#5 |
逻辑运算
1 | and R0.#3 |
1 | MOV R0, R1, LSL#2 ;将R1中的内容左移两位后传送到R0中。 |
比较两个值是否相等
1 | cmp R0,R1 |
大于和小于(带符号)
1 | ;大于 |
试想一下,我们的比较指令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标志位三者的值进行参考
总结:
cmp指令的功能相当于减法指令,只是对操作数之间运算比较,结果间接保存在标志寄存器高位中
bne,blt,bgt等这些指令都是通过获取标志寄存器中的值来得知比较结果从而进行相应跳转,不同的指令需要满足不同的条件
我们可以通过改变状态寄存器中的值来改变代码的走向
示例:
1 |
|
知识扩展:
状态寄存器的读取和写入
读取指令:MRS{mov to register from special register)
写入指令: MSR(Move to Special register from Register )
1 | # 读取状态寄存器中的值 |
状态寄存器中各个区域具体描述
一共分位四个大区域,从低到高分别为:控制位区域, 扩展位区域, 状态位区域, 标志位区域,每个区域各占8个二进制位的空间
以下是控制位区域细分详解图:
| 指令 | 含义 | 需要满足的条件 |
|---|---|---|
| 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 | GBLL Test ;声明一个全局逻辑变量Test |
根据条件的成立与否决定是否重复汇编一个程序段
若 WHILE 后面的逻辑表达式为真,则重复汇编该程序段,直到逻辑表达式为假
WHILE 和 WEND 伪指令可以嵌套使用
1 | GBLA Counter ;声明一个全局数字变量Counter |
使用import或者extern伪指令
1 | ;使用import伪指令 |
两者区别:
import:不管当前文件是否使用该引入的函数,该标签都会加入当前文件符号表,即为静态引用extern:只有当前文件使用了该函数,才会将此标签加入符号表,即为动态引用使用export或者global伪指令
1 | ;使用import伪指令 |
第一步,在汇编原文件中将函数暴露出来给供外部调用,使用export或者global伪指令:
1 | AREA code, CODE |
第二步,在C文件中引用汇编中的函数,C文件中只能使用extern伪指令:
1 | extern arm_strcpy(char *src,char*des); |
第一步,在C文件中编写好函数
1 | int c_sum(int a,int b){ |
第二步, 在汇编文件中引入函数,使用import或者extern伪指令
1 | AREA code, CODE |
第三步, 使用BL指令调用函数
1 | AREA code, CODE |
在ARM中函数参数使用R0~R3这四个寄存器来进行传递,最多传递4个参数,超过4个参数使用栈进行处理,函数返回值通过R0进行传递
C文件中代码:
1 | #include<stdio.h> |
汇编文件中代码:
1 | AREA code, CODE |
在C语言中嵌入汇编代码,格式如下:
1 | int main2(){ |
内嵌汇编的注意事项:
1 | void c_strcopy(char *src,char *des){ |
| 寄存器 | 别名 | 用途 |
|---|---|---|
| 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 | 程序计数器 |
1 | AREA test, CODE |
(一) 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-gcc是 Codesourcery 公司(目前已经被Mentor收购)基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM系统中所有环节的代码,包括裸机程序、u-boot、Linux kernel、filesystem和App应用程序。使用时,按照主机平台,可以下载以下任一版本中的一个,结果是一样的:
为方便国内用户下载使用,我们从Codesourcery(Mentor)官网下载了所有版本的arm-none-linux-gnueabi-gcc编译器,并放到国内云盘上提供下载。因为版本很多,可能难以选择,但是我们建议您使用最新版本,即有最新~标志的下载项。
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 最新~推荐!—— 下载
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 —— 最新~ 下载
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 —— 最新~ 下载
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 —— 最新~ 下载
arm-arm-none-linux-gnueabi-2007q3-51.i686.rpm —— 下载
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设备可以直接运行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]
ARM,MIPS根据对操作系统的支持与否,ARM GCC可分为支持和不支持操作系统,
ARM架构裸机,工具链提供商未知,嵌入式应用二进制接口,这个工具不支持那些跟操作系统关系密切的函数,比如fork(2)。他使用的是newlib这个专用于嵌入式系统的C库。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 | #include <stdio.h> |
在命令窗口执行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!
本文为作者原创 转载时请注明出处 谢谢

SI(source index)是源变址寄存器
DI(destination index)是目的变址寄存器
它们和[bx]类似,用于存放偏移地址
1 | assume cs:code,ds:data |
改成栈复制:
1 | assume cs:code,ds:data |
ds:[bx+数字]
1 | ds:[bx+数字] ;如果写成[数字+bx],那么相当于[bx]+数字,两者意思不一样 |
ds:[bx+si]和ds:[bx+si+数字]
ds:[bx+di]和ds:[bx+di+数字]
1 | assume cs:code ,ds:data |
1 | assume cs:code ,ds:data,ss:stack |
本文为作者原创 转载时请注明出处 谢谢

以8086为例,假如声明一个段,不论是数据段,栈段还是代码段, 一旦段里面有内容,那么会从一个新的段地址开始开辟空间,如果代码进行了分段处理,那么就会形成16字节对齐的现象
1 | assume ds:data |
如果段中的数据占用N个字节,则程序加载后,该段实际占用空间为:
1 | (N/16+1)*16 |
本质并不是因为段固定占用n16字节,而是因为段必须从一个新的地址段开始开辟空间,这就导致了我们认为*段一次最少拉升16字节的内存空间,必须为16的倍数**, 原因看如下代码:
1 |
|
内存分布如下:

我们发现data段并没有独占16个字节空间,而是让数据从一个新的16字节地址开始存入,段的作用是让数据在内存中的排列按照一定的布局进行排列,方便我们进行计算读取, 但是使用段的话明显会占用更多的内存空间
数据段,栈段,代码段 他们在内存中开辟的空间是根据代码由上到下依次分布的:
1 | assume ds:data,cs:code,ss:stack |
假如数据段的段地址是2000H 那么栈段的段地址为2001H, 代码段的段地址为:2002H 依次排列
如果我将各个段的代码位置调整一下,那么所在内存的位置也会跟着发生改变:
1 | assume ds:data,cs:code,ss:stack |
也就是说各个段的内存分布不是固定的, 和代码的编写有关系
如果不加start标记的话,汇编代码默认由上往下执行,cs+ip从上往下,所过之处,全部被当做代码处理, 因此即便你在数据段中存放指令,甚至打入代码起始标签也没有问题:
1 | assume cs:code,ds:data |
如果在数据段中加上代码起始标记:
1 | assume cs:code,ds:data |
每个段会独占一个栈地址
1 | assume cs:code,ds:data |
内存分布如下:

咱们会发现数据段,并不是从0100:0003开始开辟16个字节的空间,而是新起一个段地址从0101:0000开始开辟, 也就是说一个段占用独立的一个栈地址
假如把段去掉:
1 | assume cs:code,ds:data |
则内存分布如下:

那么代码会依次连续进行累加填充
本文为作者原创 转载时请注明出处 谢谢

大写字母 abcd对应的ascii码以及二进制:
1 | A 41H 0100 0001B |
小写字母 abcd对应的ascii码以及二进制:
1 | a 61H 0110 0001B |
大写转小写,只需二进制和0010 0000B进行或运算即可:
1 | or 0010 0000B ;大写变成小写, 小写依然还是小写 |
小写转大写,只需二进制和1101 1111B进行与运算即可:
1 | and 1101 1111B ;小写变成大写, 大写依然还是大写 |
1 | assume cs:code ,ds:data |
以上代码可使用偏移地址组合方式[bx+x]进行优化,结果为:
1 | assume cs:code ,ds:data |
本文为作者原创 转载时请注明出处 谢谢

1 | ;我是注释 |
1 | ;赋值 |
存放的数据大小根据使用的寄存器而定, 比如ax是16位寄存器,最大只能存放16位数据,也就是4位十六进制数据
十六进制数据不能以字母开头, 前面需加上0 否则编译报错
结构如下:
1 | 函数名: |
示例:
1 | print: ;函数名 |
x86架构中使用关键指令call
x86架构汇编示例:
1 | call print ;调用print函数 |
字符串的定义
起因:如果直接将字符串赋值给通用寄存器,会出现以下两个问题:
为了解决这个问题,需要使用另外一种方式,定义字符串
首先:需要先在内存中申请一块空间,可以使用伪指令db和dw
1 | db-->define byte 定义字节 读写一个数据,偏移量加1 |
示例
1 | db 'hello' ;占用五个字节的内存空间 |
如果定义数字,使用dw每个数字占用两个字节的空间, 字符串比较特殊,并不是每个字符占用两个字节,而是总长度必须是2的倍数
字符串的获取
获取字符串的数据,首先要获取到数据所对应的内存地址
那怎么获取已经定义好的地址呢?
第一步尝试: 给数据添加别名
1 | str db 'hello' |
别名中存放的是偏移地址,但是光有偏移地址还不行,还需要段地址, 段地址+偏移地址=实际物理地址,别名默认从ds寄存器中读取段地址, 但是我们并没有给ds寄存器赋过值, 这就导致我们无法获取正确的数据,因为我们不知道正确的段地址是多少?
那字符串段地址从哪里获取呢?
方法一: 直接从内存中找(仅限于调试,实际开发肯定不行)
方法二:使用段进行包裹, 段能给我们提供一个段地址(正解)
1 | data segment |
对内存中的数据进行读写
从内存中一次读取数据的多少,取决于寄存器的容器大小
1 | data segment |
思考:为什么以下写法报错:
1 | ;报错1 |
内存数据的读写是从低往高进行读写
上面使用db或者dw定义数据的方式,定义数据的同时就已经定义好了数据所在的物理地址, 如果我们想要从指定的内存地址中写入或者读取数据的话,需要借助段寄存器来实现 在8086中给我们提供了DS SS CS ES四个寄存器,理论上你使用哪一个都行,但是由于系统默认读取DS寄存器中的数据当做段地址,所以我们一般使用DS进行数据的段地址管理
如何从指定内存中读取数据
1 | ;假设我们需要从0710:0000这个物理地址中读取数据,然后存放到寄存器中 |
如何往指定内存中写入数据
1 | ;假设我们需要将数据写入0710:0000这个物理地址中 |
补充:往内存中写入数据是字节宽度还是字型宽度取决于寄存器的宽度也就是
mov ds:[0],ax或者数据的大小:mov ds:[0] 30H
1 | ; 以下是指定数据占用空间的大小 可以实现8位数据占用16个字节的空间的目的 |
8. 字符串修改和替换
1 | ;需求1 : 将内存中he修改为wo |
分段写法:
1 |
|
Loop循环指令
类似于高级语言中的while循环, 系统默认从cx寄存器中读取数据作为循环的条件,当cx中的值cx-1大于零时循环执行一次代码
示例:
1 | ;需求 : 将内存中wowowo修改为hello |
加减运算指令add和sub
1 | add ax,2 ;ax=ax+2 |
中断
顾名思义,程序运行到一半暂时断开,官方一点说就是,由于软件或者硬件信号,使得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 | data segment |
除法指令div
格式:
1 | div 寄存器 ;div bx 该寄存器表示除数 |
公式是被除数÷除数=商……余数
被除数需要预先存放在ax寄存器中,相除后商存放在ax寄存器中,余数存放在dx寄存器中
1 | mov ax,2000 ;定义被除数 |
因为ax和dx需要存放被除数和余数, 因此除数一般放在bx,cx,或者内存中
由于16位寄存器最多只能存放16位数据,假如被除数数值超过十六位,那么被除数低16位存放在ax寄存器中,高十六位存放在dx寄存器中
1 | mov dx,20 ;定义被除数高16位 |
乘法指令
1 | mov ax,100 |
内存中存放的数据都是十六进制的数据,如果这些十六进制数据被标记为数据,那么cpu在读取的时候,读到多少就是多少,如果被标记为了指令,那么cpu会将这些十六进制转成指令进行执行
cpu只是个无情的计算机器,它无法自动区分数据和指令,标记需要我们告诉cpu
那么 如何给数据进行标记呢?
1 | DS---->data segment |
都是用于存放段地址的
1 | 实际物理地址=段地址*16:偏移地址 10*10=100 |
DS寄存器用于存放数据的段地址,那么数据所对应的偏移地址可以使用bx寄存器进行存放,使用DS寄存操作的内存数据都被当成是纯数据,里面存的是什么,读出来的就是什么
1 | mov ax,3000H ;这行代码会转成16进制的数据存放到内存中 这些数据所对应的段地址默认放在ds寄存器中 ,使用ds进行读取的时候 读取的是该指令对应的16进制数据, 而不是被还原成了指令进行执行 |
CS寄存器用于存放指令所在的段地址,IP寄存器存放的是当前正在执行的指令所对应的偏移地址,所有使用CS:IP进行操作的内存数据都被当成是指令对待,读取的时候会将16进制数据转成对应的指令并执行
1 | mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000 |
SS寄存器用于存放栈空间对应的段地址,所有被SS操作的内存空间都被当成栈空间进行对待,你想让哪部分内存空间当作栈空间,完全取决于开发者,sp寄存器存放栈空间偏移量,ss和sp配合使用
1 | mov ax,0710H |
ES寄存器一般用于DS的替补,DS被占用无法使用时,临时使用ES替代,用法和DS一致
栈段里面存放也是数据和数据段无异,只不过数据排列的方式不一样,正常的排列方式是数据从低地址往高地址进行偏移存放,读取数据也是从低到高,而栈则是写入数据从高到低进行偏移,读取数据从低地址到高地址
由于这个特性,所以我们在定义一块空间作为栈空间使用时,都会先往高地址偏移一段空间
栈存储特点:
一次读写两个字节的数据
数据高地址往低地址逆序偏移存放
栈空间的声明
前面提到过,使用ss寄存器进行标记的空间为栈空间
1 | data segment |
往栈空间中写入数据
使用push指令
1 | data segment |
从栈空间读取数据
使用pop指令
1 | data segment |
思考:栈空间和数据段空间,里面存放的都是数据,那么是否可以使用SS来存放数据段的段地址呢,答案是可以的, 当SS充当数据段的时候,读写操作和DS一样,SS:[N],如果充当栈段的时候,读写操作时候时候pop和push,
由于pop和push默认以SS寄存器中的数据当做栈段地址,因此不能使用其他段寄存器充当栈段寄存器
前面咱们介绍过使用中断的方式输出字符串, 今天我们学习一种不使用中断的方式实现字符串的打印
在8086的内存地址结构中,B8000H~BFFFFH这部分的内存区域为显存区域,一旦向这个地址空间写入数据,cpu会从0号偏移地址开始读取数据然后显示输出, (每写入一次数据就从0开始读取一次)
代码尝试:
1 | start: |
在这块区域中,每个字符固定占用两个字节的空间,也就是ds:[0]和ds:[1]存放一个字符的信息,前者存放字符具体的内容,后者存放字符对应的颜色
比如:
1 | start: |
字符颜色的设置规则:
1 | 0 0 0 0 0 0 0 0 ;用8个二进制位表示字符属性 |
从高往低数,第一个二进制位表示是否显示闪烁痕迹
1 | start: |
第234个二进制位表示字符背景颜色 分别代表:RGB,即red、green、blue
1 | start: |
第5个二进制位表示字符是否高亮
1 | start: |
第678个二进制位表示字符本身的颜色 分别代表:RGB,即red、green、blue
1 | start: |
由于cpu会从0号偏移地址开始读取数据然后显示输出,因此假如你直接在6号偏移地址写入字符数据, 那么前面三个数据会以占位形式存在
1 | start: |
字符串打印
1 | data segment |
借助字符不断刷新显示的特性,可用让字符动画显示
1 | ;让字符从左往右移动 |
屏幕默认显示80x25个字符,全屏显示106x38个字符,那么可以根据这个特性,让字符上下移动
1 | ;让字符从上往下移动 |
另外, 让字符斜着移动
1 | ;让让字符斜着移动 |
补充: 在8086中系统提供了一个显示服务(Video Service)中断供我们使用,使用10H这个中断码也可以打印带有颜色属性的字符串
1 | ;示例1: |
使用16号中断码
1 | ;使用键盘控制字符移动 |
直接使用别名+[偏移量]的形式获取或者修改已经定义好的数据,比如:
1 | ;需求:将第二个字符串最后一个o替换成第一个字符串中的e |
mov ax ,str 中的str相当于是str[0],这也是为什么咱们能够直接通过别名来获取第一个字符的原因
前面我们只介绍了b.str[0],表示读取一个字节的数据,另外一种w.str[0]表示读取一个字的数据
别名str中存放的数据量更db和dw有关, 如果是db那么别名默认取一个字节数据,如果dw 别名默认取两个字节数据, 可以使用b.和w.进行数据量的调节
数组本质上就是多个数据的集合,在内存中按照一定顺序排列,角标即为每个数据的偏移值,如果使用db进行数据的定义那么两个数据之间的偏移值为1个字节,如果使用dw指令定义数据,则偏移值为2个字节
1 | data segment |
数组的打印
1 | data segment |
外中断 (硬中断):由外部设备(比如网卡,或者硬盘 键盘 鼠标)引发的中断,比如当网卡收到数据包的时候,就会发出一个中断
中断屏蔽:
cpu监听到外部中断时,第一步需要先判断IF标志位的值,如果为1则执行,如果为0则屏蔽
为什么还有中断屏蔽这么一说? 因为有些重复的外部中断并不需要响应,或者cpu正在在执行非常重要的一段指令,这些指令不能中断,一旦中途调开可能会造成系统崩溃,那么在这种情况下需要先将IF标志位置为0,屏蔽所有可屏蔽的外部信号,等这段指令执行完后,再讲IF还原回1, 这个IF标志位相当于是一个监听外部信号的开关
为了方便IF标志位的修改,8086提供了相应的指令方面我们使用
1 | cli ;相当于IF=0 |
当我们按下键盘中的一个键,那么键盘会向cpu发出一个中断信号,cpu接收到信号后判断标志位IF是否为1,如果为1,则执行这个中断
当松开按键时,同样会发出一个中断信号,也就是说点击一个键实际产生了两个中断信号
以上这一系列过程涉及到几个问题:
1. 键盘是一个外部设备,电信号发出时,cpu怎么知道这个信号是来自键盘而不是鼠标的呢?
2. cpu又如何知道你按下的是哪一个键?
解答1:
cpu除了可以对寄存器 和内存进行数据读写之外,还可以读写端口的数据,电脑上一共有65535个端口,每个端口相当于是一个数据通道,当外部键盘借助USB接口接入电脑被驱动识别后,势必有一个端口与其相连进行数据通信, 在8086中这个端口号为60H
那么如何使用汇编读写端口中的数据呢?
1 | in al,60H ; 使用in指令 从60h这个端口读取一个字节到al寄存器中去 |
从端口读写数据必须使用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 | 一面=80个磁道 |
中断
使用13H号中断
1 | ;读取磁盘中的数据到0:200H这个内存中 |
如果我们想要实现以下效果:
1 | mov bx,3333H ;假如这条这条指令数据所对应的物理地址为0710:0000 |
修改方法:
调试器直接修改
使用jmp指令
1 | jmp 0100h:8H |
或者使用标记
1 | jmp me |
使用jcxz指令
jcxz (jmp cx zero):条件转移指令,功能和jmp一样,只是需要满足条件,也就是当cx寄存器中的值为0时,进行跳转
1 | mov cx,0 |
使用retf指令
retf需要配合栈进行使用,当程序执行到retf这条指令时,会连续从栈中pop两次数据,第一次的数据赋值给CS,第二次的数据赋值给IP,那么如果我们想要跳转到指定的指令,需要将该指令的段地址和偏移地址分别push进栈中
1 | stack segment |
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 | code segment |
直接从内存中获取ip地址然后跳转
1 | call word ptr ds:[0] ;ds:[0]存放ip值 |
直接从内存中获取cs和ip地址然后跳转
1 | call dword ptr ds:[0] ;ds:[0]存放的是ip值, ds:[2]存放的是cs值 |
以上两种直接从内存中获取cs:ip的方式对于jmp指令同样有效
call指令和jmp指令的区别
jmp指令仅仅只是修改了cs:ip的值call指令除了修改cs:ip的值之外,还将下一条指令的ip值入栈,方便ret指令跳转调用iret指令:
iret指令执行,将ip和cs值pop出来进行跳转,同时还执行了popf,相当于执行了以下三步操作
1 | pop ip |
我们利用中断码段可以调用系统的功能,也就是被系统封装好的子程序
中断既然能够引导cpu临时去执行子程序,那么势必是更改了cs:ip的值,也就是在内存中存放了这个子程序的入口指令地址,通过int关键字找出来并跳转。这里有两个先决条件,一个是子程序必须提前编写好存放在内存中,二是将入口地址存放在内存的某个位置
当程序执行到int指令时,根据中断码计算出程序入口所在的物理地址,然后然后取出来赋值给cs:ip
那么怎么通过中断码计算呢?
比如 int 0h 会从0000:0000这个地址开始找出四个字节数据,由高地址往低地址分别为段地址和偏移地址
由于每个中断码需要占用四个字节空间,因此int 2H 从0000:0004开始找,以此类推
公式为:
1 | IP=中断码*4 |
配合咱们之前学的call指令 int 9h 可以用以下指令替代:
1 | int 9h |
编写子程序
1 | child: |
将子程序入口地址值存放到中断码对应的内存位置
1 | code segment |
使用中断码调用子程序
1 | code segment |
声音由震动产生,不同频率对应不同的声音,也就是音高
单个音高的声音不能叫音乐,只能叫音频,
而音乐是由不同时长的不同音高组合而成,比如100Hz的声音持续1秒紧接着200Hz的声音持续半秒,如此反复循环,那么在听感上就是一首音乐
在音乐演奏的世界里,为了表示音高,一般采用记谱的方式来代替频率,从而方便音乐家们交流,但是对于计算机而言,计算机不能识别简谱也不能识别五线谱
电信号—–>驱动——->扬声器
1 | data segment |
本文为作者原创 转载时请注明出处 谢谢
