第7章 相同的功能,不同的代码
该章节和上章实现的功能是一样。也在屏幕上输出:Label offset: number标号的十进制格式,不过用了更优雅的实现方式。
代码清单7-1
作者提供了源码,可以查看配书源码和工具的c07文件夹。
跳过非指令的数据区
书中讲的内容大概就是下图这个意思:
至于为什么这么做呢? 因为将数据声明整体放在前面,代码更加 易读。当然书籍有讲解代码分段(section)的方式,效果会更好。
在数据声明中使用字面值
该小节主要介绍了数据的统一声明和字面值使用。
数据的统一声明:小节一开始说明了在屏幕打印字符,该章和上一章实现方式的不同之处,如下图:
两种方法,那种更好呢?
字面值使用:汇编程序中可以使用字符直接声明,在编译后会转成ASCII码。
如果不能用字面值,那一开始声明的时候不得累死。
段地址的初始化
该小节介绍了段地址的运用,从而简化程序编写。
段地址的运用:因为程序运行时是被加载到 0x7c00 开始的位置。所以上一章在处理number数据时,都要加上 0x7c00。
这种方式很笨拙,该小节通过数据段地址 ds(Data Segment) 进行了优化。可以把 0x7c00 看成是段 0x07c0:0000 开始的位置。上面截图的代码改进一下就是这样:
两种方式从逻辑段的视角看待:
- 第一种数据段设置为:0x0000
- 第二种数据段设置为:0x07c0
其他段:其他段如代码段cs(Code Segment)和附加段es(Extra Segment)也类似。
一个简单的例子:
; 在屏幕上打印asm
mov ax,0xb800 ;设置es段位0xb800
mov es,ax
mov byte [es:0x00],'a' ;使用es:0x02寻址方式,最终地址:es*0x10+0x02
mov byte [es:0x02],'s'
mov byte [es:0x04],'m'
jmp $
times 510-($-$$)db 0
db 0x55,0xaa
段之间的批量数据传送
该小节主要介绍了 movsb、movsw 和 标志寄存器、 cld、std 指令,通过这些指令实现数据批量传送。
movsb(move string byte):将数据从源地址传送到目的地址,每次传送1个字节。
- 传送方向:ds:si => es:di,si(Source Index)、di(Destination Index)
- 传送次数:cx
- 正向和反向传送:标志寄存器的DF标志,0正向、1反向。
画个图例就是这个意思:
movsw(move string word):将数据从源地址传送到目的地址,每次传送1个字。
除了每次传送1个字外,其余都和 movsb 指令一致。
标志寄存器(FLAGS):用来存放各种标志信息。
cld(Clear Direction):DF标志置0,表示正向。
std(Setup Direction): DF标志置1,表示方向。
使用循环分解数位
上一章分解数位采用挨个处理的方式,本章采用循环分解的方式。
该小节介绍了 loop 指令 和 BX 寄存器。
loop:loop指令可以实现循环执行,循环次数由 cx 决定。
mov cx,5 ;表示循环5次
digit:
...
...
loop digit ;从digit继续执行,直到cx=0
loop digit编译后的机器码为:E2F7,E2表示loop指令,F7的计算方式下:
在1个字节的情况下:0x43 - 0x4C = F7。可以用计算器计算。
BX(Base Address Register):基址寄存器。
在8086处理器上,如果要用寄存器来提供偏移地址,只能使用寄存器BX、SI、DI、BP,不能使用其他寄存器。
比如寄存器
- AX是累加器(Accumulator),与它有关的指令还会做指令长度上的优化(较短);
- CX是计数器(Counter);
- DX是数据(Data)寄存器,除了作为通用寄存器使用,还专门用于和外部设备之间进行数据传送;
- SI是源索引(Source Index)寄存器;
- DI是目标索引(Destination Index)寄存器,用于数据传送操作;
计算机中的负数
前面在计算 jmp 指令和 loop 指令涉及到汇编地址的计算,其中涉及到负数的一些知识,作者就在这章节进行了介绍。更加详细和系统的知识我建议阅读刘宏伟老师的《计算机组成原理》课程。
无符号数和有符号数
该小节介绍了负数的表示形式、有符号数和无符号数的表示方法、neg 指令、截断问题、位数转换知识点。
负数的表示形式:负数相当于用 0 减去 该数正数。0不够减,要从高位借1。
有符号数和无符号数:
- 无符号数可以理解就是0和正数。
- 有符号数通过最高位进行区分:1表示负数;0表示正数。
借用 刘宏伟老师的《计算机组成原理》的一章图可以清楚的表示:
8位是这样,16位、32位、64位都类似。
neg:0 - 操作数,相当于操作数取相反数。
举个例子:
;功能:声明3个负数,并将第一个负数取反。
jmp near start
number db -1,-2,-3 ;定义3个负数,FF、FD
;计算机表示:FF、FE、FD
start:
mov ax,0x07c0
mov ds,ax
neg byte [number+0x00] ;number第一个数字取反
jmp $
times 510-($-$$)db 0
db 0x55,0xaa
编译后查看 .lst 文件:
通过Bochs查看 neg 执行之后,
截断问题:如果声明的负数超出寄存器或者内存的范围那么会被截断。
mov al,-200 ;被截断,编译后指令为:B8 38
mov ax,-200 ;不会截断,编译后指令为:B8 38 FF
为什么-200是为FF38,截断后为38,之前没搞太明白,用计算器计算一下就明白了。
位数转换:负数在8位、16位、32位表现不太一样。使用 cbw(Convert Byte to Word)和cwd(Convert Word to Double-word) 指令实现。
负数在8位、16位、32位表现方式:
cbw和cwd指令:
- cbw没有操作数,操作码为98。它的功能是,将寄存器AL中的有符号数扩展到整个寄存器AX。
- cwd也没有操作数,操作码为99。它的功能是,将寄存器AX中的有符号数扩展到DX:AX。
简单理解就是:正数补0、负数补1。这里如果借助计算器就更容易理解了。
处理器视角中的数据类型
该小节了介绍了计算机如何识别正负数及其相关的运算。
计算机如何识别正负数:计算机本身无法识别正负数,比如下面的1111_0000,计算机到底是识别位-16还是240?
相关的运算:加减法和原来一样。负数之所以用补码的方式,就是因为可以直接用加法运算进行处理。这样处理器只要设计加法电路就可以了。
举个例子:-49 + 51 = 2
列一个二进制竖式:
1100_1111
+ 0011_0011
--------------
1_0000_0010 (高位舍弃,就是2了,非常巧妙)
乘除法使用有符号位的指令 imul 和 idiv。
一个除法的例子:
mov ax,0xf0c0 ;二进制格式:1111_0000_1100_0000
mov bl,0x10 ;
div bl ;把ax看成无符号数
;最终结果为0xf0c,结果存储在ax中。
mov ax,0xf0c0 ;二进制格式:1111_0000_1100_0000
mov bl,0x10
idiv bl ;把ax看成有符号的数字进行除法运算。
;最终结果为-244,存储在ax中。
乘法指令类似。
数位的显示
该小节讲解了如何用循环的方法显示数位,其中的知识点:基址和变址的寻址方式、dec、jns。
基址和变址的寻址方式:NTEL8086处理器只允许以下几种基址寄存器和变址寄存器的组合
[bx+si] ;bx(Base Register) + si(Source Index)
[bx+di] ;bx(Base Register) + di(Destination Index)
[bp+si] ;bp(Base Pointer) + si(Source Index)
[bp+di] ;bp(Base Pointer) + di(Destination Index)
书中采用了 [bx+si] 依次寻址万、千、百、十、个位,画了个示意图表示第一次循序显示万位的思路:
后续千、百、十、个位的思路都是类似的。
dec指令:操作数自减1。类似高级语言的 i-- 。书中通过 dec 指令,在每次循环结束时将 si 减1,指向下一个要显示数位。
dec si
jns:条件转移指令(Jump Not Sign),如果标志寄存器里的有符号位SF(Sign Flag)为0(Not Sign),则执行跳转,否则不执行。
书中使用 jns show 指令判断在 si>=0 时,会继续跳转到show执行。
show:
mov al,[bx+si]
add al,0x30
mov ah,0x04
mov [es:di],ax
add di,2
dec si ;si自减1,如果si>=0,那么SF=0
jns show ;如果SF=0(即si>=0),则执行跳转。
其他标志位和条件转移指令
该小节介绍了标记寄存器的其他位。
奇偶标志位PF
PF(Parity Flag):当运算结果出来后,如果最低8位中,有偶数个为1的比特,则PF=1;否则PF=0。
进位标志CF
CF(Carry Flag):当处理器进行算术操作时,如果最高位有向前进位或借位的情况发生,则CF=1;否则CF=0。
溢出标志OF
OF(Overflow Flag):溢出标志,分无符号数和有符号数两种运算。
1.无符号数运算:
mov ah,0xff ;十进制255
add ah,2 ;(ah)=0x01,
;257结果超出ah表示范围了,高位舍弃,OF置为1。
2.有符号数运算:
上面同个例子:
mov ah,0xff ;十进制-1
add ah,2 ;(ah)=0x01,
;结果正常。OF=0
一个异常的例子:
mov ah,0x70 ;十进制112,二进制0111_0000
add ah,ah ;(ah)=-32,
;224超出ah的表示范围,结果异常。OF=1
算式:
0111_0000
+ 0111_0000
--------------
1110_0000 (高位是符号位,所以结果为-32)
现有指令对标志位的影响
指令执行的时候,根据运算的结果,可能会更改某些标志位,具体参考如下:
条件转移指令
该小节介绍了 “jcc” 指令族。
“jcc” 不是一条令,而是一个指令族(簇),功能是根据某些条件进行转移。
这类指令有挺多的,书中提供了一个常用的列表,我再网上查了一个详细指令列表:JC - Jump if Condition Is Met。
这些指令也不用记,掌握一些规律就很容易理解了。
- 成对出现:例如 jz (Jump Zero)ZF=1跳转 和 jnz(Jump Not Zero)ZF!=1跳转。
- 都是相关的英文单词:je,e表示Equal;jc,c表示Carry。
NASM编译器的 和 和 和$标记
1.$ :表示隐藏在当前行行首的标号。
2.$$:表示当前汇编节(段)的起始汇编地址。
times 510-($-$$) db 0 ; $-$$ 就可以表示前面汇编程序的长度
观察运行结果
本章程序的调试
调试命令“n”的使用
n可以自动完成循环过程。在循环体外的下一条指令前停住。
调试命令“u”的使用
用于显示指定地址之后的若干条指令。
u/10 0x7c22 ;显示包括0x7c22在内的后10条指令。
用调试命令“info”查看标志位
info flags ; 16位
info eflags ; 32位
首先,像“id、vip、vif、ac、vm、rf、nt、IOPL”这些标志,是32位处理器才有的,现在不用管它们。
然后,“of”是溢出标志;“df”是方向标志;“if”和“tf”是和中断有关的标志,第10章才能讲到;“sf”是符号标志;“zf”是零标志;“af”是辅助进位标志;“pf”是奇偶标志;“cf”是进位标志。
如果显示的标志名称是小写的,那么,说明该标志为“0”;否则,该标志的状态为“1”。
完。