第7章 相同的功能,不同的代码

第7章 相同的功能,不同的代码

该章节和上章实现的功能是一样。也在屏幕上输出:Label offset: number标号的十进制格式,不过用了更优雅的实现方式。

代码清单7-1

作者提供了源码,可以查看配书源码和工具的c07文件夹。

跳过非指令的数据区

书中讲的内容大概就是下图这个意思:
image
至于为什么这么做呢? 因为将数据声明整体放在前面,代码更加 易读。当然书籍有讲解代码分段(section)的方式,效果会更好。

在数据声明中使用字面值

该小节主要介绍了数据的统一声明字面值使用

数据的统一声明:小节一开始说明了在屏幕打印字符,该章和上一章实现方式的不同之处,如下图:
image
两种方法,那种更好呢?

字面值使用:汇编程序中可以使用字符直接声明,在编译后会转成ASCII码。
image
如果不能用字面值,那一开始声明的时候不得累死。

段地址的初始化

该小节介绍了段地址的运用,从而简化程序编写。

段地址的运用:因为程序运行时是被加载到 0x7c00 开始的位置。所以上一章在处理number数据时,都要加上 0x7c00。

image

寻址方式加上0x7c00

这种方式很笨拙,该小节通过数据段地址 ds(Data Segment) 进行了优化。可以把 0x7c00 看成是段 0x07c0:0000 开始的位置。上面截图的代码改进一下就是这样:
image

将数据段ds设为0x7c0

两种方式从逻辑段的视角看待:
image

  • 第一种数据段设置为: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

段之间的批量数据传送

该小节主要介绍了 movsbmovsw标志寄存器cldstd 指令,通过这些指令实现数据批量传送。

movsb(move string byte):将数据从源地址传送到目的地址,每次传送1个字节。

  • 传送方向:ds:si => es:di,si(Source Index)、di(Destination Index)
  • 传送次数:cx
  • 正向和反向传送:标志寄存器的DF标志,0正向、1反向。

画个图例就是这个意思:
image

movsb传送示意图

movsw(move string word):将数据从源地址传送到目的地址,每次传送1个字。
除了每次传送1个字外,其余都和 movsb 指令一致。

标志寄存器(FLAGS):用来存放各种标志信息。
image

8086处理器的标志位

cld(Clear Direction):DF标志置0,表示正向。

std(Setup Direction): DF标志置1,表示方向。

使用循环分解数位

上一章分解数位采用挨个处理的方式,本章采用循环分解的方式。
image
该小节介绍了 loop 指令 和 BX 寄存器。

loop:loop指令可以实现循环执行,循环次数由 cx 决定。

mov cx,5       ;表示循环5次
digit:
    ...
    ...
    loop digit ;从digit继续执行,直到cx=0

loop digit编译后的机器码为:E2F7,E2表示loop指令,F7的计算方式下:
image
在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。
image

有符号数和无符号数:

  • 无符号数可以理解就是0和正数。
  • 有符号数通过最高位进行区分:1表示负数;0表示正数。

借用 刘宏伟老师的《计算机组成原理》的一章图可以清楚的表示:
image

图片来源于刘宏伟老师的《计算机组成原理》

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 文件:
image
通过Bochs查看 neg 执行之后,
image

截断问题:如果声明的负数超出寄存器或者内存的范围那么会被截断。

mov al,-200 ;被截断,编译后指令为:B8 38
mov ax,-200 ;不会截断,编译后指令为:B8 38 FF

为什么-200是为FF38,截断后为38,之前没搞太明白,用计算器计算一下就明白了。
image

位数转换:负数在8位、16位、32位表现不太一样。使用 cbw(Convert Byte to Word)和cwd(Convert Word to Double-word) 指令实现。
负数在8位、16位、32位表现方式:
image
cbw和cwd指令:

  • cbw没有操作数,操作码为98。它的功能是,将寄存器AL中的有符号数扩展到整个寄存器AX。
  • cwd也没有操作数,操作码为99。它的功能是,将寄存器AX中的有符号数扩展到DX:AX。

image

简单理解就是:正数补0、负数补1。这里如果借助计算器就更容易理解了。

处理器视角中的数据类型

该小节了介绍了计算机如何识别正负数及其相关的运算

计算机如何识别正负数:计算机本身无法识别正负数,比如下面的1111_0000,计算机到底是识别位-16还是240?
image

相关的运算加减法和原来一样。负数之所以用补码的方式,就是因为可以直接用加法运算进行处理。这样处理器只要设计加法电路就可以了。

举个例子:-49 + 51 = 2

列一个二进制竖式:

   1100_1111
+  0011_0011
--------------
 1_0000_0010    (高位舍弃,就是2了,非常巧妙)

乘除法使用有符号位的指令 imulidiv

一个除法的例子:

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] 依次寻址万、千、百、十、个位,画了个示意图表示第一次循序显示万位的思路:
image

第一次循序显示万位

后续千、百、十、个位的思路都是类似的。

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),则执行跳转。

其他标志位和条件转移指令

该小节介绍了标记寄存器的其他位。
image

奇偶标志位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)

现有指令对标志位的影响

指令执行的时候,根据运算的结果,可能会更改某些标志位,具体参考如下:
image

图片来源书籍

条件转移指令

该小节介绍了 “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 ; $-$$ 就可以表示前面汇编程序的长度

观察运行结果

image

本章程序的调试

调试命令“n”的使用

n可以自动完成循环过程。在循环体外的下一条指令前停住。

调试命令“u”的使用

用于显示指定地址之后的若干条指令。

u/10 0x7c22 ;显示包括0x7c22在内的后10条指令。

image

用调试命令“info”查看标志位

info flags ; 16位
info eflags ; 32位

image
首先,像“id、vip、vif、ac、vm、rf、nt、IOPL”这些标志,是32位处理器才有的,现在不用管它们。
然后,“of”是溢出标志;“df”是方向标志;“if”和“tf”是和中断有关的标志,第10章才能讲到;“sf”是符号标志;“zf”是零标志;“af”是辅助进位标志;“pf”是奇偶标志;“cf”是进位标志。
如果显示的标志名称是小写的,那么,说明该标志为“0”;否则,该标志的状态为“1”。

完。

  • 23
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值