跳至主要內容

汇编语言一发入魂 0x03 - 更多的指令

未央大约 9 分钟汇编语言寻址方式

上一篇文章中我们学习了指令的寻址方式,实际上是借具体的代码总结了一下寻址方式。这篇文章我们将学习更多的指令,通过实际代码的讲解,找到写汇编语言代码的感觉。下面先来讲一下串操作指令。

串操作指令

含义:通过执行一条字符串操作指令,对存储器中某一个连续的内存中存放的一串字或字节均进行同样的操作,称为串操作。字符串操作指令简称为串操作指令。

所有的基本串操作指令都用寄存器si间接寻址源操作数,且假定源操作数在当前的数据段中,即源操作数首地址的物理地址由ds:si提供;而用寄存器di间接寻址目的操作数,且假定目的操作数在当前的附加段中,即目的操作数首地址的物理地址由es:di提供。显然,串操作指令的源操作数和目的操作数都在存储器中。

这两个地址的指针在每一个操作以后要自动修改,但按增量还是减量修改,取决于方向标志DF(位于标志寄存器内):若DF=0,则在每次操作后sidi作增量操作:字节操作加1,字符操作加2;若DF=1,则在每次操作后sidi作减量操作:字节操作减1,字符操作减2。因此对于串操作,需要预先设置DF的值。可以用stdcld指令分别置DF10

若源串和目的串在同一段中,可使dses指向相同数据段,即ds=es

还可以在任一串操作指令前加一个指令前缀,构成重复前级指令,通过此指令来控制串操作指令的重复执行操作。下面结合代码来讲解一下。

代码

.code16

movw $0x07c0, %ax
movw %ax, %ds

movw $0xb800, %ax
movw %ax, %es

cld
movw $message, %si
xorw %di, %di
movw message_length, %cx
rep movsb

jmp .

message:
  .byte 'H', 0xa, 'e', 0xa, 'l', 0xa, 'l', 0xa, 'o', 0xa, ' ', 0xa, 'W', 0xa, 'o', 0xa, 'r', 0xa, 'l', 0xa, 'd', 0xa
message_length:
  .word . - message

.org 510
.word 0xAA55

解释

与上一篇给出的代码open in new window的主要区别在第9~13行。

9行,使用cld指令将DF标志位置为0,表示每次操作后对sidi做增量操作。

10行,将message的地址赋值给si。此时引导扇区整体被BIOS加载到0x7c00处,并且我们已经将数据段设置成了0x07c0message代表数据的偏移量,该指令执行后ds:si就指向了我们的数据首地址。

11行,将di0。此时es内容为0xb800es:di表示的物理地址为0xb8000,即显存映射在内存中的首地址。

12行,设置循环次数,循环的次数为数据串的长度。

13行使用串传送指令movs来完成数据传送的工作。该指令具体分为两条movsbmovsw,分别为把由si作为指针的源操作数串中的一个字节或字,传送至由di作为指针的目的操作数串中,且根据DF修改各自的指针,使其指向各串中的下一单元。这里是把ds:si处的一个字节传送到es:di,并且把sidi分别加一。指令前缀rep是重复前缀,其功能是重复执行rep后紧跟着的一个串操作指令,直到cx寄存器中的值为0。执行时先检查cx的值,若为0则退出重复操作,执行以下其他指令;若不为0,则将cx的值减一;然后执行rep右侧的串指令;重复上述操作。

通过组合repmovs我们就可以批量的把数据从内存的一个区域转移到另一个区域。

运行

$ as --32 boot.s -o boot.o
$ objcopy -O binary -j .text boot.o boot.bin
$ qemu-system-i386 boot.bin
boot_qemu
boot_qemu

下面继续学习算术运算指令。

算数运算指令

之前我们学习过inc指令,该指令用于对操作数加一,并把结果放回到目的操作数中。此指令可以进行字节操作或字操作,其操作数可以是寄存器操作数或内存操作数。下面我们结合一个在屏幕上打印数字的程序来学习一下其他的算术运算指令。在该示例中,我们会将数字9527的每一位分解出来,并打印在屏幕上。

代码

.code16

.set DIVIDEND, 9527         # 被除数
.set DIVISOR, 10            # 除数
.set COUNT_OF_DIGITS, 4     # 位数 -- 分解需要的循环次数

movw $0x07c0, %ax
movw %ax, %ds

movw $0xb800, %ax
movw %ax, %es

# 设置 32位 被除数
# 高 16位 在 %dx 中, 低 16位 在 %ax 中
# 因为 %ax 足够保存 9527, 所以将高 16位(%dx) 清空
xorw %dx, %dx
movw $DIVIDEND, %ax
movw $DIVISOR, %cx
movw $store, %bx

# 初始化索引寄存器 (倒序保存各个数位)
movw $COUNT_OF_DIGITS - 1, %si
split:
  divw %cx
  # 除法指令执行后 商保存在 %ax 中, 余数保存在 %dx 中
  # 因为除数是 10, 所以余数小于 10, 即 %dl 中就是余数
  movb %dl, (%bx, %si)
  xorw %dx, %dx
  decw %si
  jns split

movw $COUNT_OF_DIGITS, %cx
xorw %si, %si
xorw %di, %di
putc:
  movb store(%si), %al
  orw $0x0a30, %ax
  movw %ax, %es:(%di)
  incw %si
  addw $2, %di
  loop putc

jmp .

store:
  .byte 0, 0, 0, 0

.org 510
.word 0xAA55

解释

3、4、5行使用.set伪指令定义了三个符号。符号在编译时会被编译器替换成实际的值,类似于c语言中的#define指令。

16、17、18行初始化被除数和除数。当对字执行div操作时,需要将被除数放在dx:ax中,高字节在dx中,低字节在ax中。我们的被除数是9527ax能够容纳,所以直接将dx0,并将9527移动到ax中就完成了对被除数的准备工作。div指令的源操作数,即除数,可以是除立即数之外的任何类型的操作数。这里我们使用寄存器cx存储div指令的源操作数。

19行将store的地址移动到bx基址寄存器中,因为我们打算展示一下基址加变址寻址的应用。store开始的4个字节空间是为分解9524的四个位而保留的。

22行我们将索引寄存器si的值设置为COUNT_OF_DIGITS - 1,即3。此时bx + si的值为store + 3,即相对于store3个字节。因为依次分解出来的是个、十、百、千位,如果顺序保存在store处的话稍后打印就需要倒序打印,所以我们在保存的时候就倒序保存在内存中,方便稍后打印。

24行执行divw除法指令,除法指令完成后会将商保存在ax中,余数保存在dx中。

27行将dl中的数据保存到内存数据段bx + si处。因为除数是10,所以余数是小于10的,dl就足够保存我们需要的数据。根据上面的分析,此时bx + si等于store + 3,即我们会将分解出来的数据保存在store开始的第3个字节处(从0开始计数)。

28行将dx0,为下一次分解做准备。

29行使用dec指令将索引寄存器si减一,此时bx + si等于store + 2,即表示从store开始的第2个字节处。下一个数位保存在这里。

30行使用条件转移指令jns来实现循环分解各位。jns是一个条件转移指令,当结果为正时(SF=0)转移。SF是状态标志寄存器FLAGS中的符号标志位(Sign Flag)。用于表示符号数的正负。如果运算结果的最高位为1,则SF=1,否则为0。因为上一条指令dec可以影响到符号标志位,当si为负数的时候我们就可以知道分解已经完成,从而跳出分解数位的过程,执行后续指令。

32行设置循环次数,我们将通过循环将分解好的每一位打印在屏幕上。

33、34行将索引寄存器sidi0,我们将使用这两个寄存器分别访问分解好的位和显存对应的内存地址。

36行将分解好的位移动到al中。这里有一个隐含的条件,当被除数被分解完的时候,ax寄存器保存的是除法操作的商,此时商为0,即ax寄存器的值为0

37or按位逻辑或指令的功能有两个,一是将ax寄存器的高字节设置为0x0a,表示打印的字符的显示属性,浅绿色;二是将ax寄存器的低字节加上0x30,因为分解好的数字并不能直接打印在屏幕上,屏幕上打印的数字实则是数字对应的ASCII码。观察这里给出的ASCII码表,我们可以发现,数字对应的ASCII码为数字本身加上十六进制的0x30

38行将要打印的字符连同显示属性一起转移到显存对应的内存处。

39行将si1,指向下一个位数。

40行将di2,因为我们在第38行中一次操作了两个字节的数据。

46行为分解的结果保留了4个字节的空间,每个字节用于保存一个位。

运行

$ as --32 boot1.s -o boot1.o
$ objcopy -O binary -j .text boot1.o boot1.bin
$ qemu-system-i386 boot1.bin
boot1_qemu
boot1_qemu

总结

伪指令

  • .set 用于定义一个符号,类似于c语言中的#define指令。

指令

  • cld 用于将标志寄存器FLAGSDF标志位置为0
  • movs 串传送指令,用于将数据从ds:si处移动到es:di处,一次可移动一个字节或一个字;根据DF决定移动完成之后sidi加/减12。配合rep重复前缀和cx完成批量传送。
  • div 无符号除法指令。当源操作数(除数)为字节时,除法指令的功能是ax 除以 源操作数,商存入al,余数存入ah;当源操作数为字时,除法指令的功能是dx:ax 除以 源操作数,商存入ax,余数存入dxdx:ax表示由这两个寄存器共同组成的数据,dx保存其高位,ax保存低位。
  • or 按位逻辑或指令。
  • add 普通加法指令,无进位。
  • dec 减一指令。
  • jns 条件转移指令。当标志寄存器符号标志位SF0时转移。