call 指令调用方式

一、

汇编语言 --寄存器-指令集-寻址_寄存器的汇编语言有哪些-CSDN博客

二、原文链接:https://www.jianshu.com/p/55726f7e355a

当我们使用高级语言调用一个函数 func() 时,在编译为汇编代码后,实际上是调用了 call 指令。伪代码如下:

call func

默认的 call 调用是 near 近调用。聪明的你可能想到,既然有近调用,那么肯定有远调用了。今天我们就来说说 call 在 x86 的 16 位 实模式下的几种调用方式。

开门见山,先列出 call 调用的 4 种方式:

  • 相对近调用
  • 间接绝对近调用
  • 直接绝对远调用
  • 间接绝对远调用

可以看到,上面的几种调用方式种有几组反义词,间接/直接,近/远,绝对/相对。字面上都很好理解,肯定也都是跟地址相关的,那么具体到调用层面,是怎么处理的呢?我们一一来讲解。

在讲述之前,我们需要明确一个概念。在实模式中,CS 寄存器中存放的是段基址,IP 寄存器中存放的是段内偏移量,内存物理地址 = 段基址 + 段内偏移

相对近调用

  • 近调用:指调用的函数在同一个段中,无需跨段,即 cs 不变。
  • 相对:指待调用函数地址相对于下一条指令(即 call 的下一条),计算出一个偏移量。也就是说这个偏移量不是真正的段内偏移,而是相对位置偏移量。

指令格式如下,默认为 near,因此 near 可以省略。

call near 立即数地址

call 中的立即数地址也就是对应着相对位置偏移量。所以要想获取真正的偏移,还需经过一番计算。

由于 x86小端字节序,即 高位在高地址,低位在低地址。它对应的机器码是 e8llhh,大小为 3 字节,其中 e8 代表相对近调用。ll 表示立即数的低位,hh 表示立即数的高位。

假设立即数为 0x1234,34 在低位,12 在低位。机器码如下:

e83412

假设知道了相对偏移量,那么被调用函数实际段内偏移地址计算方式如下:

被调用函数实际段内偏移 = 下一条指令地址 + 相对偏移量

下一条指令地址 = 当前指令地址 + 当前指令长度

// 最终结果
被调用函数实际段内偏移 = 当前指令地址 + 当前指令长度 + 相对偏移量

由于是相对近调用,编译器需要算出相对偏移量,根据上述公式,可得出:

相对偏移量 = 被调用函数实际段内偏移(也就是函数地址) - (当前指令地址 + 当前指令长度)

举个栗子,假设被调用函数的地址为 0x12,call 指令的地址为 0x3,那么相对偏移量为 0x12 - 0x3 - 3 = 0x6

0x6 填充为 2 字节表示:0x0006,后两位 06 是低位,前两位 00 是高位,因此机器码可表示为:e80600,从左往右地址增大。

实例

为了让大家理解得更清晰,我们通过一个例子讲解下。call_1.S 的代码如下,每行添加了相应注释。

;近调用 call_proc
call near near_proc

; $ 表示当前行,即不断循环
jmp $

;定义变量 addr,初始值为 0x4
addr dd 4

;定义 near_proc 函数
near_proc:

;将 0x1234 放入 ax 寄存器
mov ax, 0x1234

;返回
ret
  1. nasm -o call_1.bin call_1.S 将其编译,生成机器码。

  2. 使用 xxd 来逐字节查看 call_1.bin 中的内容。如何使用 xxd 查看字节,可参看 辅助工具

    输入如下命令:

    // 0 - 起始字节
    // 13 - 查看的字节长度
    ./xxd.sh call_1.bin 0 13 
    

    输出如下:

    00000000: E8 06 00 EB FE 04 00 00 00 B8 34 12 C3           ..........4..
    

上面我们说到,这种方式的机器码为 e8llhh。一眼可以看出,第一个字节就是 e8,后面的 0x0006 就是立即数。但是如何验证它所调用函数的地址,是通过我们提到的公式计算得到的呢?

我们将生成的机器码反汇编一下,ndisasm call_1.bin,输出如下结果:

00000000  E80600            call 0x9
00000003  EBFE              jmp short 0x3
00000005  0400              add al,0x0
00000007  0000              add [bx+si],al
00000009  B83412            mov ax,0x1234
0000000C  C3                ret
  • 第一列是文件偏移,当没有设置编址基址时可认为它是地址。编址基址在第二种调用方式种会提到。
  • 第二列是机器码指令。
  • 第三列是汇编代码。

从上可以看到,第一条指令为 E80600,对应汇编代码 call 0x9,也就是说调用函数的地址是 0x9。它的相对偏移量为 0x0006,我们再根据 被调用函数实际段内偏移 = 当前指令地址 + 当前指令长度 + 相对偏移量 这个公式计算出实际偏移量,看是否能跟反汇编中的结果匹配。

// 当前指令地址 0,指令长度 3,相对偏移 0x6
实际偏移 = 0 + 3 + 0x6 = 0x9

得到实际偏移为 0x9,计算出的偏移量跟反汇编得到的偏移是吻合的,验证通过~

再看一下这条指令,可以得知 mov ax 的操作码是 b8。这里提到它是让大家先有个印象,因为后面会用到。

00000009  B83412            mov ax,0x1234

间接绝对近调用

  • 间接:顾名思义,即不能直接使用的数据。需要通过寄存器寻址或者是内存寻址,从寄存器/内存地址中获取地址。
  • 绝对:即为真实段内偏移,无需再次计算。

指令格式可分为寄存器内存地址两种方式:

// 通过寄存器
call ax

// 通过地址
call [addr]
  • call [addr],内存寻址。它的操作码为 ff16,整条指令的机器码是 ff16+16位内存地址
  • call 寄存器,寄存器寻址。随着寄存器不同,操作码也不一样。比如 call ax 的机器码是 ffd0call cx 的机器码是 ffd1

这种方式比相对近调用要简单一些,从寄存器/内存中获取地址即可。

实例

下面我们用一个栗子来讲解一下。call_2.S 代码如下,每句代码都添加了相应注释。

;自定义 section,名字为 call_test,告诉汇编器从 0x900 开始编址,之后地址逐个+1
section call_test vstart=0x900

;将 near_proc 函数地址写入 addr 变量中,word 表示 2 字节,即将 near_proc 地址以 2 字节表示。
mov word [addr], near_proc

;调用 near_proc
call [addr]

;将 near_proc 的地址放入 ax 寄存器
mov ax, near_proc

;调用 near_proc
call ax

; $ 代表当前指令地址,这句表示跳转到当前指令,即不断循环
jmp $

;定义变量 addr,4 字节,初始值为 0x4
addr dd 4

;定义 near_proc 函数
near_proc:

;将 0x1234 放入 ax 寄存器
mov ax, 0x1234

同样,先将其编译为机器码,然后使用 xxd 查看字节,内容如下:

00000000: C7 06 11 09 15 09 FF 16 11 09 B8 15 09 FF D0 EB  ................
00000010: FE 04 00 00 00 B8 34 12 C3                       ......4..

稍微瞄一眼,我们可以看到有两个 ff 的指令,这也对应着汇编代码的两次 call 调用。

  • 一次是 call [addr],对应指令为 ff161109addr = 0x0911
  • 另一次是 call ax,对应指令为 ffd0

同样我们将其反汇编,得到如下结果:

00000000  C70611091509      mov word [0x911],0x915
00000006  FF161109          call [0x911]
0000000A  B81509            mov ax,0x915
0000000D  FFD0              call ax
0000000F  EBFE              jmp short 0xf
00000011  0400              add al,0x0
00000013  0000              add [bx+si],al
00000015  B83412            mov ax,0x1234
00000018  C3                ret

下面我们来对反汇编的代码进行分析,看其地址是否跟该调用方式一致。

  1. 首先我们看 mov ax,0x1234 这一行,它是函数的起始行,其文件偏移是 0x15。由于我们设置了 vstart=0x900,那么函数的地址为 0x900 + 0x15 = 0x915

  2. 第一行指令 mov word [0x911],0x915。这行指令的含义比较清楚,就是将数值 0x915 放入 0x911 这个地址中,因此 0x911 的内容就是 0x915

  3. 在调用第一个 call 指令 call [0x911] 时,它指向的地址是 0x911,而 0x911 中的内容恰恰是 0x915,也就是函数地址。因此,内存寻址这种方式验证通过~

  4. 第三行指令 mov ax,0x915,给 ax 赋值 0x915,即函数地址。

  5. 在调用第二个 call 指令 call ax 时,同样会调用到该函数。因此,寄存器寻址也验证通过~

直接绝对远调用

  • 直接:表示操作数是立即数,直接可使用。
  • 源调用:表示跨段访问,也就是 cs 和 ip 都需要改变。

指令格式如下,far 可加可不加,但返回必须用 retf,表示远返回。

// 段基址和段内偏移都是立即数
call far 段基址: 段内偏移

操作码是 0x9a,整条指令格式为 0x9a + 段内偏移(2 字节) + 段基址(2 字节)

由于 cs 和 ip 都需要改变,因此在调用函数时,cs 和 ip 均要压栈,以便函数返回时恢复。

实例

call_3.S 代码如下:

;从 0x900 开始编址
section call_test vstart=0x900

;直接绝对远调用 far_proc,0表示段基址,far_proc 表示偏移地址。far 可加可不加,call far 0:far_proc 也可以。
call 0:far_proc

;死循环
jmp $

;函数定义
far_proc:
mov ax, 0x1234

;配合远调用使用
retf

先将其编译为机器码,然后使用 xxd 查看字节。结果如下所示:

00000000: 9A 07 09 00 00 EB FE B8 34 12 CB                 ........4..

很明显,我们可以看出第一个字节为 9a,表示直接远调用。紧跟着的 2 字节为段内偏移 0x0907,而后跟着的 2 字节为段基址 0x0000。所以其实际地址为 0000: 0907 = 0x907

这次机器码比较简短,我们不用反汇编的方式,直接通过机器码来看。

函数定义是 mov ax, 0x1234,上面提到它所对应的指令为 B83412。这条指令的文件偏移为 7,可以自己数一下,9A 对应 0,B8 对应 7。同样由于编址基址的原因,加上 0x900,那么其实际地址为 0x907,验证通过~。

间接绝对远调用

再一次出现「间接」,这里我们应该能猜到它的含义。也就是说段基址和段内偏移需要从寄存器/内存中取出。

不过它只支持内存寻址,不使用寄存器寻址的原因可能是一下要用两个寄存器,太浪费资源。

指令格式如下所示,注意需要添加 far,否则会跟第二种调用方式混淆。

call far [addr]

操作码是 ff1e,后面跟着内存地址 addr。它表示从 addr 地址中取出数据,低两个字节是段内偏移,高两个字节是段基址。

实例

cal_4.S 代码如下:

;从 0x900 开始编址
section call_test vstart=0x900

;间接远调用
call far [addr]

;死循环
jmp $

;定义 addr 变量,大小为 4 字节。低 2 字节为段内偏移,即 far_proc 地址;高 2 字节为段基址,0。
addr dw far_proc, 0

far_proc:
  mov ax, 0x1234
  retf

先将其编译为机器码,然后使用 xxd 查看字节。结果如下所示:

00000000: FF 1E 06 09 EB FE 0A 09 00 00 B8 34 12 CB        ...........4..

其中 ff1e0906,表示间接远调用,0x0906 为内存地址,相对编址基址 0x900 距离为 6,我们要从 0x0906 中取出 4 字节数据。那如何取呢?

第一个字节 FF 对应 0,从前往后数到 6,即对应着 0A,再取出 4 字节,内容为 0x0000090a,这就是函数实际地址。根据内容可计算出段基址为 0,段内偏移为 0x090a

同样,根据上一种方式的套路,B83412 对应的文件偏移为 10,也就是 0xa。再加上编址基址 0x900,即为 0x90a,同样验证通过~。

写在最后

这篇文章中,我们介绍了 call 指令的几种调用方式,举出具体代码示例说明其使用方式,并查看了相应机器码。然后通过反汇编或者直接查看机器码的方式,进一步验证了原理与实际指令布局的契合。


原文链接:https://www.jianshu.com/p/55726f7e355a

  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值