Windows下如何确定虚函数在虚函数表中的位置
我需要用c#调用 c++ 的 类的函数, 虽然可以通过头文件的顺序,但是如果可以打印出虚函数在虚表中的Offset更好。
测试要求:
Windows, x86 只有1层虚函数,没有被override过
虚函数调用如下
auto a_reqCreditDetail = &XtTraderApi::reqCreditDetail;
(api->*a_directOrder)(NULL, 0, NULL);
通过VisualStudio的汇编模式观察,
a_reqCreditDetail 其实是一个thunk, 他指向了一条语句:
jmp xti::XtTraderApi::vcall'{4}' (0926C75h)
//E9 Offset1 Offset2 Offset3 Offset4
//E9 A1 58 00 00
E9 表示一个短跳转,偏移量就是后面 4个字节组成的数值
经过这个跳转,来到新的代码块,在这里,才是调用虚函数表,调用虚函数:
//跳转到调用虚拟函数的代码块
mov eax, dword ptr [ecx]
//8B 01
jmp dword ptr [eax + x]
//FF 60 X1
//FF A0 X1 X2 X3 X4
这里ecx指向的就是类, 也是类的虚函数表
之后他就会jmp 到虚函数
我们则需要得到这个 x 的值, 他就表示虚函数在虚函数表中byte的offset
需要指出的一点是 jmp dword ptr [eax + x] 对应的语句分别是
FF 60 X1
FF A0 X1 X2 X3 X4
前者是1个字节表示X,后者是3个字节表示X
我们完成工具函数:
int DetectVFTableOffset(void* p_thunk_fn)
{
auto vftable_offset = -1;
//p_thunk_fn 指向一个 近跳转
//jmp xti::XtTraderApi::vcall'{4}' (0926C75h)
//E9 Offset1 Offset2 Offset3 Offset4
//E9 A1 58 00 00
//然后跳转到调用虚拟函数的代码块
//mov eax, dword ptr [ecx]
//8B 01
//jmp dword ptr [eax + x]
//FF 60 X1
//FF A0 X1 X2 X3 X4
auto jmp_o1_o2_o3_o4 = *(char*)p_thunk_fn;
if (jmp_o1_o2_o3_o4 != (char)0xE9)
{
throw std::exception("没有找到类似 jmp xti::XtTraderApi::vcall'{4}' (0926C75h) 的语句");
}
auto offset = *(int*)((char*)p_thunk_fn + 1);
auto new_address = (char*)p_thunk_fn + 5 + offset;
auto mov_eax_dword_ptr_ecx = *(short*)new_address;
if (mov_eax_dword_ptr_ecx != 0x018B)
{
//抛出异常
throw std::exception("没有找到语句mov eax, dword ptr [ecx]");
}
//确定jmp dword ptr [eax + x]
auto jmp_ff = *((char*)new_address + 2);
if (jmp_ff != (char)0xFF)
{
//抛出异常
throw std::exception("没有找到类似 jmp dword ptr [eax + x]的语句");
}
//确定下一句是60,还是A0
auto byte_60_A0 = *((char*)new_address + 3);
if (byte_60_A0 == 0x60)
{
//FF 60 X1
vftable_offset = *((char*)new_address + 4);
}
else if (byte_60_A0 == (char)0xA0)
{
//FF A0 X1 X2 X3 X4
vftable_offset = *(int*)((char*)new_address + 4);
}
else
{
//抛出异常
throw std::exception("没有找到类似 jmp dword ptr [eax + x]的语句, 预期的数据不是60或者A0");
}
return vftable_offset;
}
还有一个小问题,就是如何把 a_reqCreditDetail 变成 void*
auto a_reqCreditDetail = &XtTraderApi::reqCreditDetail;
a_reqCreditDetail是一个复杂的虚函数指针。
但是在底层汇编中, 我们知道, a_reqCreditDetail 指向的是一个thunk代码块( 这个代码块只有1条jmp语句)
但是我无法在c++层面把他优雅的转为void*
我只能用如下的语句实现
template<typename src_type>
void* pointer_cast(src_type src)
{
void* ret = NULL;
__asm
{
mov eax, src
mov ret, eax
}
return ret;
}
这个函数使用__asm在底层把a_reqCreditDetail转为了 void*
最后为了方便我wrap了两者,生成了新的函数
template<typename src_type>
int DetectVFTableOffsetEx(src_type src)
{
auto p_thunk_fn = pointer_cast(src);
return DetectVFTableOffset(p_thunk_fn);
}
这样我的调用看上去是这样的:
auto vfo_setCallback = DetectVFTableOffsetEx(&XtTraderApi::setCallback);
auto vfo_init = DetectVFTableOffsetEx(&XtTraderApi::init);
auto vfo_initSync = DetectVFTableOffsetEx(&XtTraderApi::initSync);
auto vfo_join = DetectVFTableOffsetEx(&XtTraderApi::join);
auto vfo_join_async = DetectVFTableOffsetEx(&XtTraderApi::join_async);
auto vfo_destroy = DetectVFTableOffsetEx(&XtTraderApi::destroy);
这样我就不用写很麻烦的转换,元编程给我解决了。
P.S ChatGPT 为我解释了机器语言我把对话内容贴出来,给大家参考:
翻译为机器码: mov eax,dword ptr [ecx]
ChatGPT
在x86汇编语言中,指令 mov eax, dword ptr [ecx] 用于将ECX寄存器中的地址指向的32位(dword,即double word)值移动到EAX寄存器中。这条指令是数据传输指令的一部分,用于读取内存内容。
当这条指令被转换成机器码时,它会被编码为一系列字节,这些字节在执行时由CPU解释为上述操作。对于这条具体的指令,其机器码(以Intel x86架构为例)将取决于指定的操作数和操作模式(例如,保护模式下的32位操作)。
一个可能的机器码示例为(请注意,实际的机器码可以根据上下文和具体的指令格式有所不同):
Copy code
8B 01
这里,8B 是操作码(opcode)表示MOV指令的一个变种,后续的01是指令的ModR/M字节,这里指定了源操作数(source operand)是通过ECX寄存器间接寻址的,并且目标操作数(destination operand)是EAX寄存器。
解析机器码:
8B : MOV指令的操作码,表示这是一个从内存到寄存器的移动操作。
01 : ModR/M字节,指定了具体的寄存器和寻址模式。在这个例子中,它指示EAX是目标寄存器,而源数据来自ECX指向的内存地址。
请注意,实际使用中机器码的具体值可能因编译器、指令的上下文(如地址模式、段重载前缀等)以及目标平台的具体实现而有所不同。上述机器码仅为典型情况下的示例,用于说明mov eax, dword ptr [ecx]这条指令可能的二进制表示。在具体环境下,建议使用反汇编工具来获取精确的机器码。
User
机器码: jmp dword ptr [eax+4]
ChatGPT
指令 jmp dword ptr [eax+4] 在x86汇编中表示无条件跳转到一个地址,这个地址是通过寄存器 EAX 加上偏移量 4 来确定,并从那个计算得到的内存位置读取跳转目标地址。这种类型的跳转通常用于实现多态调用、函数指针调用等高级特性。
将这条汇编指令转换为机器码的确切表示取决于多个因素,包括具体的处理器架构和编译器。然而,对于基于Intel x86的架构,这条指令通常会编译为以下形式的机器码:
Copy code
FF 60 04
这里是如何解析这个机器码的:
FF 是操作码(opcode),表示这是一个间接跳转(jmp)指令的一部分。
60 是ModR/M字节,指示这是一个基于寄存器加偏移量的间接寻址模式。
04 是偏移量,表示从 EAX 指向的地址开始,向前移动4个字节来找到跳转目标的地址。
说明
这个机器码序列使得CPU跳转到由 EAX 寄存器加4字节偏移所指向的内存地址中存储的地址处执行。这通常用在虚函数调用中,其中 EAX 可能指向一个对象的虚函数表(vtable),而具体的偏移量 4 指向表中的某个特定函数的地址。通过这种方式,可以实现根据对象的动态类型来选择调用哪个函数,这是实现多态的基础。
请注意,具体的机器码可能因编译器的不同优化选项、目标平台的具体架构等因素而有所不同。上述机器码只是在典型情况下的一种可能表示。要获得准确的机器码表示,最好使用反汇编工具直接分析特定编译输出的二进制文件。
User
机器码: jmp dword ptr [eax+2ACh]
ChatGPT
指令 jmp dword ptr [eax+2ACh] 在x86汇编中执行一个无条件跳转到由 EAX 寄存器加上偏移量 2ACh(十进制为684)指向的内存地址处。这个地址中存储的值成为新的指令指针(即跳转目标)。这种跳转方式常见于基于表的跳转,如虚函数调用或其他间接调用中,其中 EAX 通常指向某种结构(如虚表)的开始,而具体的偏移量则指向该结构中的特定条目。
将此汇编指令转换为机器码涉及到指定间接跳转的操作码,以及指定基址加偏移的寻址模式。对于Intel x86架构,大致的机器码形式可能如下:
Copy code
FF A0 AC 02 00 00
这里的机器码分解如下:
FF 是执行间接跳转指令的操作码。
A0 是ModR/M字节,它指定了使用基址加偏移的寻址方式,并且目标是间接通过内存地址给出的。
AC 02 00 00 是偏移量 2ACh 的小端表示(即,最低有效字节在前)。在小端存储约定中,地址 000002AC 实际上会被写作 AC 02 00 00。
注意事项:
真实的机器码和寻址方式可能会因具体的编译器、编译选项、目标架构的不同而有所变化。特别是在不同的处理器模式(如x86-64模式)中,指令的编码和处理可能会有显著差异。
上述机器码表示适用于32位x86架构。对于其他架构或模式(如x86-64),机器码会有所不同。
解析和理解机器码通常需要具备较深入的底层编程和架构知识。在实际开发中,除非在进行底层系统开发、逆向工程或特定类型的性能优化,否则一般不需要手动处理机器码。
要获得特定程序或指令的确切机器码表示,使用专业的反汇编工具或调试器查看编译后的二进制代码是最直接的方法。
User
为何与机器码 jmp dword ptr [eax+4] 差别那么大? 一个是 FF 60 04
ChatGPT
我之前的解释可能有些误导,对此我感到抱歉。让我来澄清一下。
对于间接跳转指令(如 jmp dword ptr [eax+4] 和 jmp dword ptr [eax+2ACh]),确实存在一定的格式差异,主要是偏移量的大小影响了指令的编码方式。
对于较小的偏移量(例如4),指令可能使用更短的编码形式。而对于较大的偏移量(如2ACh),则需要额外的字节来表示偏移量,因此会使得整个机器码变长。
以 jmp dword ptr [eax+2ACh] 为例,正确的解析应当考虑到偏移量2ACh(十进制的684)如何影响机器码的表示。
对于 jmp dword ptr [eax+4],一个更准确的机器码表示(假设是基于某些具体的编译器和设置)应该是像 FF 60 04 这样,这里:
FF 是间接跳转的操作码。
60(实际上应该是一个包含ModR/M字节信息的值,这里我之前的解释有误,正确的值取决于具体的寻址模式和偏移量)。
04 是紧接着操作码的立即数,表示从基址寄存器指向的地址开始加上的偏移量。
而对于 jmp dword ptr [eax+2ACh],考虑到2ACh是一个较大的偏移量,不能简单地用单个字节来表示,所以需要更多的字节。指令格式可能更接近 FF A0 AC 02 00 00,这里:
FF 同样表示这是一个间接跳转指令。
A0 或者相近的值(我之前的解释可能不够精确)指示了这是一个基址加偏移的寻址方式。
AC 02 00 00 是偏移量2ACh的具体值,以小端格式表示。
重要的是要理解,具体的机器码编码方式会受到多种因素的影响,包括但不限于寻址方式、偏移量的大小、目标处理器架构等。为了得到最准确的机器码,最好的方式是使用具体的编译器和反汇编工具直接生成或查看特定指令的编码。
User
详细说说 jmp 语句转为 机器码
ChatGPT
jmp 指令是汇编语言中用于实现无条件跳转的指令。在x86架构下,这个指令有多种形式,可以直接跳转到指定的地址,也可以间接跳转到一个寄存器或内存地址中存储的地址。我们将通过一些例子来探讨如何将不同类型的 jmp 指令转换为机器码。
直接跳转(Near Jump)
对于形式如 jmp 0x00401000 的直接跳转指令,它指示程序跳转到具体的地址执行。这种类型的跳转通常用于同一代码段内的跳转。
在32位模式下,这类指令的机器码可能以 E9 开头,后跟一个相对偏移量,这个偏移量是目标地址与跳转指令下一条指令地址之间的差值。例如:
css
Copy code
E9 [相对偏移量]
如果假设 jmp 0x00401000 指令的地址是 0x00401005,则相对偏移量将是 0x00401000 - (0x00401005 + 5)(这里的 5 是因为 E9 指令加上四字节偏移量的总长度)。
间接跳转
间接跳转可以基于寄存器,如 jmp eax,或基于内存地址,如 jmp dword ptr [eax+4]。间接跳转让跳转的目的地更加灵活,常见于虚函数调用、跳转表等场景。
基于寄存器的间接跳转
形式如 jmp eax 的指令表示跳转到 EAX 寄存器中存储的地址。这类指令的机器码通常由两部分组成:操作码和寄存器指示码。例如,jmp eax 的机器码可能是:
Copy code
FF E0
这里,FF 是操作码,表示间接跳转,而 E0 是ModR/M字节,表示使用 EAX 寄存器作为跳转地址。
基于内存的间接跳转
形式如 jmp dword ptr [eax+4] 的指令表示跳转到由 EAX 寄存器加上 4 偏移量指向的内存地址中存储的地址。这类指令的机器码同样分为操作码和寻址模式指示,例如:
Copy code
FF 60 04
这里,FF 是操作码,60 是ModR/M字节,表示基于 EAX 的间接寻址,04 是具体的偏移量。
注意事项
实际的机器码可能会根据具体的编译器、汇编器、目标架构(x86还是x86_64)、以及指令的具体使用场景有所不同。上面给出的机器码仅为示例,用于说明不同类型的 jmp 指令如何被编码。
获取准确的机器码通常需要利用汇编器(如NASM、MASM)将汇编代码转换为机器码,或者使用反汇编工具(如IDA Pro、Ghidra)查看编译好的二进制文件中的机器码。
User
ModR/M字节
ChatGPT
在x86架构的汇编语言中,ModR/M字节是一个关键组成部分,用于指定指令操作的寄存器和内存地址。它广泛用于指令编码中,以提供关于操作数(即指令作用的对象)的详细信息。ModR/M字节由三个部分组成:Mod字段、Reg/Opcode字段和R/M字段。
组成
ModR/M字节共8位,按照位的分布,可以划分为三个部分:
Mod(2位):最高的两位,指示操作数的类型(寄存器或内存地址)及寻址方式。它也决定了是否使用SIB字节(Scale-Index-Base,当寻址复杂时使用),以及是否包含位移(disp8或disp32)。
Reg/Opcode(3位):中间的三位,根据上下文,这部分要么用来指定一个寄存器,要么作为操作码的一部分。
R/M(3位):最低的三位,用来指定一个寄存器或者,结合Mod字段,表示内存寻址方式。
Mod字段值及含义
00:表示操作数是直接通过内存地址给出,或者(当R/M字段为100时)使用SIB字节寻址,或者(当R/M字段为101时)直接使用32位偏移量(disp32)。
01:表示操作数通过寄存器间接寻址,附加一个8位偏移量(disp8)。
10:表示操作数通过寄存器间接寻址,附加一个32位偏移量(disp32)。
11:表示操作数是直接在寄存器中,此时不使用内存寻址。
示例
假设有一条指令需要使用ModR/M字节来指定操作数,我们使用add eax, [ebx+4]作为例子。这条指令表示将EBX寄存器指向的内存地址加上偏移量4的内容加到EAX寄存器中。
Mod字段:因为有一个8位偏移量,所以Mod为01。
Reg/Opcode字段:因为add操作目的地是EAX寄存器,EAX寄存器的编码为000,所以这部分也是000。
R/M字段:操作源是基于EBX的偏移,EBX的编码为011。
综合起来,ModR/M字节为01000011(二进制),或43(十六进制)。
注意
实际的指令编码可能更复杂,包括使用SIB字节指定基址、索引和比例因子进行更复杂的内存寻址,或者根据具体指令包含立即数、位移等额外信息。理解和使用ModR/M字节需要熟悉指令集架构和具体的寻址模式。在汇编和反汇编工作中,正确解析ModR/M字节对于理解和生成正确的机器码至关重要。
User
Reg/Opcode
ChatGPT
在x86架构的指令编码中,ModR/M字节是一个关键的编码组件,用于指定操作数(即指令作用的对象),其中包含了Mod字段、Reg/Opcode字段和R/M字段。Reg/Opcode字段是这个字节中的一部分,具有不同的作用,取决于具体的指令上下文。
Reg/Opcode 字段
Reg/Opcode字段占据ModR/M字节的中间3位。它可以有两种用途:
指定寄存器(Reg用途):在很多指令中,这个字段直接指定了一个寄存器,这个寄存器可以是源操作数、目的操作数,或者即是源操作数又是目的操作数之一。寄存器的指定遵循一定的编码规则,例如,000代表EAX/AX/AL(具体取决于操作数大小),001代表ECX/CX/CL,以此类推。
扩展操作码(Opcode用途):对于一些指令,这3位并不指定寄存器,而是作为操作码的一部分,用来扩展指令的操作。这在一些如位操作(bit test、bit set等)和特定的算术操作指令中比较常见。
举例说明
作为寄存器指定:例如,add eax, edx指令中,eax是目的操作数,而edx是源操作数。如果这条指令使用ModR/M字节来编码,Reg/Opcode字段将用于指示EDX寄存器,因为EDX的寄存器编码是010。
作为操作码的一部分:指令test eax, imm32(测试EAX和一个立即数),其中test指令本身有一个基本的操作码,而ModR/M字节中的Reg/Opcode字段用作操作码的一部分,以区分test指令与其他可能共享同一基本操作码的指令。
编码规则
寄存器编码:x86架构定义了一组通用寄存器的编码规则,例如,EAX是000,ECX是001,依此类推。这种编码同时适用于32位(如EAX)、16位(如AX)和8位(如AL)寄存器。
操作码扩展:当Reg/Opcode字段作为操作码的一部分时,它扩展了基本操作码,允许编码更多的指令。例如,在进行算术运算时,这可以帮助区分是加法、减法还是按位与操作。
总结
Reg/Opcode字段的具体意义和用途取决于指令本身的设计。在阅读或写入机器码时,正确解释这一字段对于理解指令的行为至关重要。不同的指令可能会以不同的方式使用这一字段,这就要求开发者或分析师熟悉x86指令集的细节。
User
jmp dword ptr [eax+4] 的机器码是 FF 60 04, 讲解60 ,特别是其中 reg/opcode
ChatGPT
在x86架构下,指令jmp dword ptr [eax+4]是一条间接跳转指令,其目标地址存储在以eax寄存器加4偏移处的内存位置。这条指令的机器码是FF 60 04。这里,我们重点讨论中间的字节60,特别是其中的Reg/Opcode部分。
机器码解析
FF是操作码(Opcode)部分,表示这是一条间接跳转指令。
60是ModR/M字节,用于指示操作数的具体寻址方式和相关寄存器。
04是指令中的立即数部分,表示相对于eax的偏移量。
ModR/M字节(60)
ModR/M字节的格式如下,共8位:
最高的两位(Mod字段)指定寻址方式。
中间的三位(Reg/Opcode字段)扩展操作码或指定寄存器。
最低的三位(R/M字段)指定基寄存器或使用特定寻址模式。
对于60(二进制表示为01100000):
Mod字段:01表示基寄存器加8位有符号位移(这里实际上用的是32位位移,由指令的上下文确定)。
Reg/Opcode字段:100,在这个上下文中,这部分并不是用来指示一个具体的寄存器,而是和操作码一起决定了操作的类型。对于FF操作码,100表示这是一条间接跳转(jmp)到内存地址的指令。
R/M字段:000,表示这个地址是基于EAX寄存器的。
在jmp dword ptr [eax+4]这条指令中,60字节通过其Mod和R/M部分指出了跳转目标地址基于EAX寄存器加上一个立即数偏移(在这条指令中偏移量由下一个字节04给出)。Reg/Opcode字段为100,与操作码FF结合,表明这是一个间接跳转。
特别关注Reg/Opcode
在FF 60 04这个机器码中,Reg/Opcode字段(100)与jmp指令的操作码FF共同决定了操作的具体类型——即间接跳转到由[eax+4]给出的地址。在这个上下文中,Reg/Opcode字段不直接指定寄存器,而是作为操作码的一部分,指示了这是一个间接跳转指令。这显示了Reg/Opcode字段在指令编码中的灵活性:它既可以用于指定寄存器,也可以扩展基本操作码,以支持更多的指令。
User
机器码 jmp xti::XtTraderApi::`vcall'{4}' (0926C75h)
ChatGPT
在x86架构中,对于直接跳转(即跳转到硬编码的地址)和间接跳转(即跳转到寄存器或内存地址中存储的地址)的指令,机器码的表示形式会有所不同。你提到的jmp xti::XtTraderApi::vcall'{4}' (0926C75h)看起来像是想要直接跳转到一个特定的地址(在这个例子中是0926C75h`),但实际在编写汇编语言时,我们不直接编码目标地址。相反,我们使用符号名称(如函数名),编译器或链接器会解析这些符号到它们相应的地址。
当我们在高级语言(如C++)中看到像xti::XtTraderApi::vcall'{4}'这样的表达式时,它代表一个虚函数调用的地址。但是在汇编级别,虚函数的调用通常是通过虚表(vtable)来实现的,即通过对象的内存布局中的虚表指针(vptr)间接访问。直接的跳转到虚函数地址(如jmp 0926C75h)在标准的x86汇编中并不是一个常见的操作,因为这要求编译时已经知道了虚函数的最终地址,这与虚函数的动态绑定特性相冲突。
不过,如果我们确实需要在汇编中硬编码跳转到某个地址,比如说我们已经知道了某个函数在运行时的确切地址,并且我们想要直接跳转到那里,这通常会使用直接跳转的机器码。在x86汇编中,直接跳转的指令有多种形式,包括短跳转(jmp short)、近跳转(jmp near)和远跳转(jmp far),它们的机器码格式各不相同。
例如,一个近跳转(jmp)到具体地址的指令可能会被编译成相对偏移形式(基于当前指令地址之后的偏移量),而不是直接编码为目标地址。如果我们假设jmp 0926C75h是一个有效的操作,并且编译器支持直接编码这个地址,它的机器码可能会依赖于指令的具体类型和目标地址。
不过,请注意,直接通过机器码硬编码跳转到虚函数通常是不可行的,因为虚函数的地址在程序运行前是不确定的,它们依赖于运行时的动态链接。如果你需要在汇编中调用C++的虚函数,正确的做法是通过对象的虚表进行间接调用。
User
机器码 E9 A1 58 00 00, 翻译成汇编语言
ChatGPT
机器码 E9 A1 58 00 00 表示一个近跳转(jmp)指令,这是一种相对跳转,意味着它会将当前指令指针(EIP寄存器的值)加上一个相对偏移量来计算跳转的目标地址。E9是近跳转指令的操作码,后面的四字节A1 58 00 00表示相对偏移量。
为了将这个机器码翻译成汇编语言,我们需要解码相对偏移量。在这个例子中,偏移量以小端格式存储,这意味着最低有效字节在前。因此,偏移量为0x000058A1。这个偏移量是从跳转指令的下一条指令开始计算的,即从E9指令后的第一字节开始计算到跳转目标的偏移。
假设E9 A1 58 00 00出现在地址0x1000(这里只是一个假设的示例地址),则跳转的目标地址计算如下:
跳转指令的地址:0x1000
指令长度:5 字节(E9加上四字节的偏移)
下一条指令的地址:0x1000 + 0x5 = 0x1005
相对偏移量:0x000058A1
目标地址:0x1005 + 0x000058A1 = 0x158A6
因此,对应的汇编指令是从指令所在地址跳转到0x158A6。在具体的汇编代码中,这通常会被表示为:
assembly
Copy code
jmp 0x158A6 ; 假设的目标地址
或者,如果你是在分析某个具体的可执行文件或正在编写汇编代码,你会根据实际的基地址和偏移来计算目标地址,然后使用符号标签来表示跳转目标。
User
如果只看到 E9 , 就知道完整的语句是 5 个字节吗
ChatGPT
是的,如果你在x86架构下看到机器码以E9开头,你就可以知道这是一条近跳转(jmp)指令,其完整的指令长度将是5个字节。这5个字节由E9操作码本身加上后面的4个字节组成,这4个字节表示相对偏移量。
在x86汇编中,近跳转指令jmp使用相对偏移量来计算跳转目标地址,该偏移量是相对于当前指令指针(EIP寄存器的值)加上jmp指令本身的长度(即E9后的第一字节开始)计算得到的。因此,当处理器执行到一个以E9开头的指令时,它会读取后面的4个字节作为偏移量,并据此计算出要跳转到的目标地址。