曲曲的秘密学术基地

纯化欲望、坚持严肃性

欢迎!我是曲泽慧(@zququ),目前在深圳(ICBI,BCBDI,SIAT)任职助理研究员。


病毒学、免疫学及结构生物学背景,可以在 RG 上找到我已发表的论文

本站自2019年7月已访问web counter

汇编语言 10.7-10.10 call和ret的配合使用 mul 模块化 参数和结果传递的问题

10.7 call和ret的配合使用

问题(1) 下面程序返回前,bx中的值是多少?

assume cs:code
code segment
start: mov ax, 1
       mov cx, 3
       call s
       mov bx, ax
       mov ax, 4c00h
       int 21h
    s: add ax, ax
       loop s
       ret
code ends
end start

分析程序运行的过程:

  1. CPU将call s 指令的机器码读入,IP指向了call s后的指令mov bx, ax,然后CPU执行call s指令,将当前的IP值(指令mov bx, ax的偏移地址)压栈,并将IP的改变为标号处s处的偏移地址。
  2. CPU从标号s处开始执行指令,loop循环完毕后,(ax)=8;
  3. CPU将ret指令的机器码读入,IP指向了ret指令后的内存单元,然后CPU执行ret指令,从栈中弹出一个值(即call s先前压入的mov bx, ax指令的偏移地址)送入IP中。则CS:IP指向指令mov bx, ax
  4. CPU从mov bx, ax开始执行指令,直至完成。

程序返回前,(bx)=8。可以看出,从标号s到ret的程序段的作用是计算2的N次方,计算前,N的值由cx提供。

再从机器码的角度观察一下栈的变化。

代码示例, stack segment db 8 dup (0) 1000:0000 00 00 00 00 00 00 00 00 db 8 dup (0) 1000:0008 00 00 00 00 00 00 00 00 stack ends

code segment:

编号 代码 地址 机器码
1 start:mov ax, stack 1001:0000 B8 00 10
2 mov ss, ax 1001:0003 8E D0
3 mov sp, 16 1001:0005 BC 10 00
4 mov ax, 1000 1001:0008 B8 E8 03
5 call s 1001:000B E8 05 00
6 mov ax, 4c00h 1001:000E B8 00 4C
7 int 21h 1001:0011 CD 21
8 s:add ax, ax 1001:0013 03 C0
9 ret 1001:0015 C3

code ends end start

分析:

  1. 首先当执行1~3行代码,会对栈进行清空,这是指针指向栈顶:
    1000:0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00(指针)
  2. 第5行执行call指令E8 05 00后,IP = 000EH,并将此IP压入栈中:
    1000:0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0E(指针) 00
    并行跳转(IP)=(IP)+0005=0013H
  3. CPU从cs:0013H处(即标号s处)开始执行
  4. 到9后执行ret指令后,机器码C3,进行pop IP,执行后,栈中情况如下:
    1000:0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0E 00(指针)
  5. CPU回到cs:000EH处,继续执行。

从上面的例子与讨论中,我们可以发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行。call指令转去子程序之前,call指令后面的指令的地址将存储在栈中,所以可在子程序的后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行。

这样,我们可以利用call和ret来实现子程序的机制。先通过写一段C语言代码,然后进行反汇编:

#include <stdio.h>

void love(void);

int main()
{
    printf("aaaaaaaa\n");
    love();
}

void love()
{
    printf("Then, bbbbbbb\n");
}

反汇编结果如下,

test main:
    0x100000f20 <+0>:  pushq  %rbp
    0x100000f21 <+1>:  movq   %rsp, %rbp
    0x100000f24 <+4>:  subq   $0x10, %rsp
    0x100000f28 <+8>:  leaq   0x63(%rip), %rdi          ; "aaaaaaaa\n"
    0x100000f2f <+15>: movb   $0x0, %al
    0x100000f31 <+17>: callq  0x100000f70               ; symbol stub for: printf
    0x100000f36 <+22>: movl   %eax, -0x4(%rbp)
    0x100000f39 <+25>: callq  0x100000f50               ; love at main.c:14
    0x100000f3e <+30>: xorl   %eax, %eax
    0x100000f40 <+32>: addq   $0x10, %rsp
    0x100000f44 <+36>: popq   %rbp
    0x100000f45 <+37>: retq
test love:
    0x100000f50 <+0>:  pushq  %rbp
    0x100000f51 <+1>:  movq   %rsp, %rbp
    0x100000f54 <+4>:  subq   $0x10, %rsp
    0x100000f58 <+8>:  leaq   0x3d(%rip), %rdi          ; "Then, bbbbbbb\n"
    0x100000f5f <+15>: movb   $0x0, %al
    0x100000f61 <+17>: callq  0x100000f70               ; symbol stub for: printf
    0x100000f66 <+22>: movl   %eax, -0x4(%rbp)
    0x100000f69 <+25>: addq   $0x10, %rsp
    0x100000f6d <+29>: popq   %rbp
    0x100000f6e <+30>: retq

从以上反汇编结果总结具有子程序的源程序的框架如下:

assume cs:code
code segment
  main: :
        :
        call sub1       ;调用子程序sub1
        :
        :
        mov ax, 4c00h
        int 21h

  sub1: :               ;子程序sub1
        :
        call sub2       ;调用子程序sub2
        :
        :
        ret             ;子程序返回至main

  sub2: :
        :
        :
        ret             ;子程序返回至sub1
code ends
end main

10.8 mul 指令

mul指令总结起来共有三种形式,

  1. 8位数与8位数相乘:
    这两个8位数一个放在AL中,另一个放在8位reg或内存字节单元中。
    结果默认放在ax中。因为8位和8位的乘法不会超过16位。
  2. 16位数与16位数相乘:
    这两个16位数一个放在AX中,另一个放在16位reg或内存字单元中。
    结果高位默认在DX中存放,低位在AX中放。
  3. 8位数与16位相乘:
    其中一个8位数按16位处理,并按16位数乘法规则进行。

代码示例,

mul byte ptr ds:[0]

含义:

$(ax)=(al)((ds)16+0)$

mul word ptr [bx+si+8]

含义:

结果的低16位存放在 $(ax)=(ax)((ds)16+(bx)+(si)+8)$

结果的高16位存放在 $(dx)=(ax)((ds)16+(bx)+(si)+8)$

举例说明,

(1) 计算100*10

100和10小于255,可以做8位乘法,程序如下,

mov al, 100
mov bl, 10
mul bl

结果:(ax) = 1000 (03E8H)

(2) 计算100*10000

100小于255,可10000大于255,所以必须做16位乘法,程序如下,

mov ax, 100
mov bx, 10000
mul bx

结果:(ax)=4240H,(dx)=000FH (F4240H = 1000000)

10.9 模块化程序设计

call与ret指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序模块化是必不可少的。因为现实的问题比较复杂,对现实问题进行分析时,把它转化成为相互联系、不同层次的子问题,是必须的解决方法。而call与ret指令对这种分析方法提供了程序实现上的支持。利用call和ret指令,我们可以用简捷的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。

以下,我们来看一下子程序设计中的相关问题和解决方法。

10.10 参数和结果传递问题

子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者

其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值

比如设计一个子程序,可以根据提供的N,来计算N的3次方。

这里有两个问题:

  1. 将参数N存储在什么地方?
  2. 计算得到的数值,存储在什么地方?

对于该模块,代码如下:

;说明:计算N的3次方
;参数:(bx)=N
;结果:(dx:ax)=N^3

cube: mov ax, bx
      mul bx
      mul bx
      ret

用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者或子程序的读写操作恰恰相反:

  1. 调用者将参数送入参数寄存器,从结果寄存器中取到返回值;
  2. 子程序从参数寄存器中取到参数,将返回值送入结果寄存器中。

下面编程,计算data段中的第一组数据的3次方,结果保存在后面一组dword单元中。

assume cs:code
  data segment
    dw 1,2,3,4,5,6,7,8
    dw 0,0,0,0,0,0,0,0
  data ends

  code segment
    start: mov ax, data
           mov ds, ax
           mov si, 0
           mov di, 16

           mov cx, 8
        s: mov bx, [si]
           call cube
           mov [di], ax
           mov [di].2, dx

           add si, 2
           add di, 4
           loop s

           mov ax, 4c00h
           int 21h

     cube: mov bx, ax
           mul bx
           mul bx
           ret

code ends
end start

注意一开始对于内存单元的分配,决定了对于后面整个程序的把握。

Last One

汇编语言 10.11 10.12 批量数据的传递 寄存器冲突问题

10.11 批量数据的传递子程序中如果只要一个参数,可以放在寄存器bx中。但是如果应对多个数据需要传递,由于寄存器的数量终究有限,我们不能简单地用寄存器存放多个需要传递的数据。对于返回值,也同样有相同的问题。这种情况,我们将批量数据放到内存中,然后将它们所在的内存空间地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可以用同样的方法。问题: 设计一个子程序,功能:将一个全是字母的字符串转化为大写。分析:这个子程序需要知道两件事,字符串的内容和字符串的长度。因为字符串中的字...…

汇编语言More
Next One

C语言 如何利用mac xcode实现C语言反汇编

今天谈一下,如何利用mac系统下的xcode实现类似于windows系统下microsoft visual C++的反汇编功能。(1) 首先安装xcode(2) 按顺序在菜单栏目依次选择:file –> new –> Project…(3) 在新弹出的菜单栏中在最上面选择macos标签页下的Command Line Tool图标,如图所示:(4) 在新弹出的菜单栏中的Language选项栏中选择C,(5) 主界面上左侧菜单栏中会出现两个文件夹分别存放代码以及编译好的程序,(6)...…

C语言More