80x86调用函数指令是啥

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了80x86调用函数指令是啥相关的知识,希望对你有一定的参考价值。

CALL - 调用过程
操作码 指令 说明
E8 cw
CALL rel16
相对近调用,位移量相对于下一条指令

E8 cd
CALL rel32
相对近调用,位移量相对于下一条指令

FF /2
CALL r/m16
绝对间接近调用,地址由 r/m16 给出

FF /2
CALL r/m32
绝对间接近调用,地址由 r/m32 给出

9A cd
CALL ptr16:16
绝对远调用,地址由操作数给出

9A cp
CALL ptr16:32
绝对远调用,地址由操作数给出

FF /3
CALL m16:16
绝对间接远调用,地址由 m16:16 给出

FF /3
CALL m16:32
绝对间接远调用,地址由 m16:32 给出

说明
将过程链接信息保存到堆栈上,并分支到目标(调用目标)操作数指定的过程(被调用过程)。目标操作数指定被调用过程中第一条指令的地址。此操作数可以是立即数、通用寄存器或内存位置。

此指令可用于执行四种不同类型的调用:

近调用 - 调用当前代码段(CS 寄存器当前指向的段)中的过程,有时称为段内调用。

远调用 - 调用当前代码段之外的段中的过程,有时称为段间调用。

特权级别间远调用 - 对特权级别与当前执行程序或过程不同的段中的过程进行的远调用。

任务切换 - 调用不同任务中的过程。

后两种调用类型(特权级别间调用与任务切换)只能在保护模式中执行。如需有关近调用、远调用及特权级别间调用的详细信息,请参阅“IA-32 英特尔(R) 体系结构软件开发人员手册”第 1 卷第 6 章中标题为“使用 Call 与 RET 调用过程”的部分。如需有关使用 CALL 指令执行任务切换的详细信息,请参阅“IA-32 英特尔(R) 体系结构软件开发人员手册”第 3 卷第 6 章“任务管理”。

近调用。执行近调用时,处理器将 EIP 寄存器的值(包含 CALL 指令后面的指令的偏移量)压入堆栈(稍后用作返回指令指针)。然后,处理器分支到当前代码段中由目标操作数指定的地址。目标操作数指定代码段中的绝对偏移量(即相对于代码段基址的偏移量)或相对偏移量(相对于 EIP 寄存器中指令指针的当前值的有符号位移量,此指针指向 CALL 指令后面的指令)。执行近调用时,CS 寄存器保持不变。

对于近调用,绝对偏移量在通用寄存器或内存位置(r/m16 或 r/m32)中间接指定。操作数大小属性确定目标操作数的大小(16 位或 32 位)。绝对偏移量直接加载到 EIP 寄存器。如果操作数大小属性是 16,则 EIP 寄存器的两个高位字节清除为零,得到大小最大为 16 位的指令指针。(使用堆栈指针 [ESP] 作为基址寄存器来间接访问绝对偏移量时,使用的基址值是 ESP 在指令执行之前的值)。

在汇编代码中,相对偏移量(rel16 或 rel32)通常指定为标签,但是在机器代码级别,它的编码形式是有符号的 16 位或 32 位立即数。此值会加到 EIP 寄存器中的值上。对于绝对偏移量,操作数大小属性确定目标操作数的大小(16 位或 32 位)。

实地址模式或虚 8086 模式中的远调用。在实地址模式或虚 8086 模式中执行远调用时,处理器将 CS 与 EIP 寄存器的当前值压入堆栈,作为返回指令指针使用。然后,处理器执行指向目标操作数指定的代码段与偏移量的“远分支”操作,以便调用被调用过程。这里,绝对远地址由目标操作数使用指针(ptr16:16 或 ptr16:32)直接指定,或是使用内存位置(m16:16 或 m16:32)间接指定。使用指针方法时,被调用过程的段与偏移量在指令中编码,编码时使用 4 字节(16 位操作数大小)或 6 字节(32 位操作数大小)远地址立即数。使用间接方法时,目标操作数指定内存位置,它包含 4 字节(16 位操作数大小)或 6 字节(32 位操作数大小)远地址。操作数大小属性确定远地址中偏移量的大小(16 位或 32 位)。远地址直接加载到 CS 与 EIP 寄存器。如果操作数大小属性为 16,则 EIP 寄存器的两个高位字节清除为零。

保护模式中的远调用。处理器在保护模式中操作时,CALL 指令可用于执行以下三种类型的远调用:

相同特权级别远调用。不同特权级别远调用(特权级别间调用)。任务切换(远调用另一项任务)。

在保护模式中,处理器总是使用远地址中的段选择器部分访问 GDT 或 LDT 中相应的描述符。描述符类型(代码段、调用门、任务门或 TSS)与访问权限确定要执行的调用操作类型。

如果所选描述符是代码段的,则执行相同特权级别代码段远调用。(如果选择的代码段在另一个特权级别中,并且代码段为非相容代码段,则生成一般保护性异常)。在保护模式中执行的相同特权级别远调用与在实地址模式或虚 8086 模式中执行的远调用非常相似。绝对远地址由目标操作数使用指针(ptr16:16 或 ptr16:32)直接指定,或是使用内存位置(m16:16 或 m16:32)间接指定。操作数大小属性确定远地址中偏移量的大小(16 位或 32 位)。新的代码段选择器及其描述符加载到 CS 寄存器,相对于指令的偏移量加载到 EIP 寄存器。

请注意,调用门(在下一段叙述)也可用于执行相同特权级别上代码段的远调用。此机制提供了另一层面的间接调用,进行 16 位与 32 位代码段之间的调用时,首选这种方法。

执行特权级别间远调用时,被调用过程的代码段必须通过调用门访问。目标操作数指定的段选择器确定调用门。同样地,在这里,目标操作数可以使用指针(ptr16:16 或 ptr16:32)直接指定调用门的段选择器,或是使用内存位置(m16:16 或 m16:32)间接进行指定。处理器从调用门描述符中获取新代码段的段选择器与新的指令指针(偏移量)。(使用调用门时,忽略目标操作数的偏移量)。执行特权级别间调用时,处理器会切换到被调用过程的特权级别的堆栈。新堆栈段的段选择器在当前运行的任务的 TSS 中指定。执行堆栈切换之后,分支到新的代码段。(请注意,使用调用门对相同特权级别的段执行远调用时,不会发生堆栈切换)。在新堆栈中,处理器会压入以下值:调用过程堆栈的段选择器与堆栈指针、调用过程堆栈的一组参数(可选),以及调用过程代码段的段选择器与指令指针。(调用门描述符的值确定要将多少个参数复制到新的堆栈)。最后,处理器分支到新代码段中被调用过程的地址。

使用 CALL 指令执行任务切换与通过调用门执行调用存在一定程度的相似。这里,目标操作数指定要切换到的任务的任务门段选择器(忽略目标操作数中的偏移量)。任务门则指向任务的 TSS,它包含任务代码与堆栈段的段选择器。TSS 还包含挂起任务之前要执行的下一条指令的 EIP 值。此指令指针值加载到 EIP 寄存器,以便任务从这个下一条指令再次执行。

CALL 指令也可直接指定 TSS 的段选择器,这样就不用间接通过任务门。如需有关任务切换机制的详细信息,请参阅“IA-32 英特尔® 体系结构软件开发人员手册”第 3 卷第 6 章“任务管理”。

请注意,使用 CALL 指令执行任务切换时,会将 EFLAGS 寄存器中的嵌套任务标志 (NT) 设置为 1,并且会同时加载新 TSS 的前一个任务链接字段与旧任务的 TSS 选择器。可以预见,代码会通过执行 IRET 指令暂停此嵌套任务,由于 NT 标志已设置为 1,此指令将自动使用前一个任务链接返回到调用任务。(如需有关嵌套任务的详细信息,请参阅“IA-32 英特尔® 体系结构软件开发人员手册”第 3 卷第 6 章“任务链接”)。使用 CALL 指令切换任务与 JMP 指令在这一点上是不同的,JMP 指令不会将 NT 标志设置为 1,因此 IRET 指令应该不会暂停任务。

16 位与 32 位混合调用。在 16 位与 32 位代码段之间执行远调用时,应该通过调用门进行。如果是从 32 位代码段到 16 位代码段的远调用,则应该从 32 位代码段的头 64 KB 执行调用。这是因为指令的操作数大小属性设置为 16,所以只能保存 16 位返回地址偏移量。另外,应该使用 16 位调用门执行调用,以便将 16 位值压入堆栈。如需有关在 16 位与 32 位代码段之间执行调用的详细信息,请参阅“IA-32 英特尔(R) 体系结构软件开发人员手册”第 3 卷第 16 章“16 位与 32 位混合代码”。
参考技术A 调用子程序用call,返回用ret. 参考技术B call 参考技术C call function_name 参考技术D 建议你自己用一下吧。建立两个程序,让他们都输出东西。在一个进程中exec另一个程序。
用重定向到某个文件的方式调用父进程,看看什么情况
请参考
第5个回答  2010-11-10 建议你自己用一下吧。建立两个程序,让他们都输出东西。在一个进程中exec另一个程序。
用重定向到某个文件的方式调用父进程,看看什么情况
请参考

为啥 64 位 VC++ 编译器在函数调用后添加 nop 指令?

【中文标题】为啥 64 位 VC++ 编译器在函数调用后添加 nop 指令?【英文标题】:Why does 64-bit VC++ compiler add nop instruction after function calls?为什么 64 位 VC++ 编译器在函数调用后添加 nop 指令? 【发布时间】:2017-06-30 20:37:47 【问题描述】:

我使用 Visual Studio C++ 2008 SP1,x64C++ 编译器编译了以下内容:

我很好奇,为什么编译器会在那些 calls 之后添加那些 nop 指令?

PS1。我知道第二个和第三个 nops 将在 4 字节边距上对齐代码,但第一个 nop 打破了这个假设。

PS2。编译的 C++ 代码中没有循环或特殊优化内容:

CTestDlg::CTestDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CTestDlg::IDD, pParent)

    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);

    //This makes no sense. I used it to set a debugger breakpoint
    ::GdiFlush();
    srand(::GetTickCount());

PS3。 附加信息: 首先,感谢大家的意见。

以下是补充意见:

    我的第一个猜测是incremental linking 可能与它有关。但是,项目的Visual Studio 中的Release 构建设置关闭了incremental linking

    这似乎只影响x64 构建。以x86(或Win32)构建的相同代码没有nops,即使使用的指令非常相似:

    我尝试使用更新的链接器构建它,尽管VS 2013 生成的x64 代码看起来有些不同,但它仍然在一些nops 之后添加了nops:

    另外dynamicstatic 链接到MFC 对nops 的存在没有影响。这是使用VS 2013 动态链接到 MFC dll 构建的:

    还要注意那些nops 也可以出现在nearfar calls 之后,它们与对齐无关。这是我从IDA 获得的部分代码,如果我再往前走一点:

如您所见,nop 插入在 far call 之后,恰好“对齐”B 地址上的下一个 lea 指令!如果这些只是为了对齐而添加的,那是没有意义的。

    我最初倾向于相信,因为nearrelativecalls(即以E8开头的那些)是somewhat faster而不是farcalls(或以calls开头的那些) FF,15 在这种情况下)

链接器可能会先尝试使用nearcalls,因为这些比farcalls 短一个字节,如果成功,它可能会用nops 填充剩余空间在末尾。但是上面的例子(5)有点推翻了这个假设。

所以我仍然没有明确的答案。

【问题讨论】:

填充对齐? 看起来很像 RIP 相关的间接调用,链接器对直接调用放宽了——间接调用在 x86 上要长一个字节,因此链接器插入了 nop 以使它们具有相同的长度。跨度> 我怀疑@Fanael 是正确的。摆脱那个 NOP 意味着改变所有的代码。但是转移所有代码会改变很多地址。似乎是通过 NOP 解决的先有鸡还是先有蛋的问题。 @Mysticial Weird 这对于 ELF 如何进行(动态)链接来说是不必要的。 @fuz 使用 ELF 函数调用共享库中的函数总是通过存根函数进行。在 Windows 上,对 DLL 中的函数的函数调用通常是使用间接调用指令直接进行的。上面反汇编中的call cs:LoadIconW 指令就是一个例子。反汇编程序调用LoadIconW 的位置包含一个指向实际LoadIconW 函数的指针。 【参考方案1】:

这纯粹是一种猜测,但它可能是某种 SEH 优化。我说 优化 因为 SEH 似乎在没有 NOP 的情况下也能正常工作。 NOP 可能有助于加快展开。

在以下示例 (live demo with VC2017) 中,在调用 basic_string::assign 后插入了一个 NOP test1 但不在 test2 中(相同但声明为非抛出1)。

#include <stdio.h>
#include <string>

int test1() 
  std::string s = "a";  // NOP insterted here
  s += getchar();
  return (int)s.length();


int test2() throw() 
  std::string s = "a";
  s += getchar();
  return (int)s.length();


int main()

  return test1() + test2();

组装:

test1:
    . . .
    call     std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
    npad     1         ; nop
    call     getchar
    . . .
test2:
    . . .
    call     std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
    call     getchar

请注意,MSVS 默认使用/EHsc 标志(同步异常处理)进行编译。如果没有该标志,NOPs 就会消失,而使用/EHa(同步异步异常处理),throw() 将不再有任何区别,因为 SEH 始终处于打开状态。


1 出于某种原因,只有throw() 似乎减少了代码大小,使用noexcept 会使生成的代码更大,并召唤更多NOPs。 MSVC...

【讨论】:

【参考方案2】:

这是一个特殊的填充物,让异常处理程序/展开函数能够正确检测它是否是函数的序言/尾声/主体。

【讨论】:

【参考方案3】:

这是由于 x64 中的调用约定要求堆栈在任何调用指令之前对齐 16 个字节。这不是(据我所知)硬件要求,而是软件要求。这提供了一种方法来确保在进入函数时(即在调用指令之后),堆栈指针的值始终为 8 模 16。因此允许简单的数据对齐和从堆栈中对齐的位置存储/读取。

【讨论】:

虽然你的说法是正确的,但与我原来的问题无关。 您问为什么将 nop 添加到程序集中,这就是原因。这怎么不能回答你的问题? 我问为什么在call 指令之后添加nops 而不是函数本身。您提到的适用于函数的主体(或代码),而不是 call 指令。 NOP 可以填充代码以对齐RIP(但在这种情况下不是这样;请查看转储中的代码地址)。调用约定要求对齐RSP。 NOP 不会修改RSP

以上是关于80x86调用函数指令是啥的主要内容,如果未能解决你的问题,请参考以下文章

在指令中调用函数

delphi中函数的声明和调用是啥意思?

调用函数的最佳方法是啥?

在 TypeScript Angular 指令中调用函数?

ES6 类构造函数不能作为普通函数调用的原因是啥?

06-系统调用(执行过程访管指令库函数与系统调用)