反汇编——结构体作为局部变量的传参和返回值的本质测试

本文探讨了结构体作为参数传递及作为函数返回值时的不同实现方式。通过VS2017与VC++6.0的反汇编对比,详细分析了结构体参数传递过程中的寄存器使用与内存复制两种方法,以及结构体作为返回值时的地址传递机制。

不同编译器生成的结果不同,但本质都是通过寄存器或内存复制,本测试只是为了了解结构体在作为局部变量时的参数传递与返回的本质,实际使用中,最好定义为全局变量

结构体传参:

# include<stdio.h>

struct st
{
	char a;
	short b;
	int c;
	int d;
	int e;
};
void Function(st s)
{

}
int main(int argc, char* argv[])
{
	st s;
	s.a = 1;
	s.b = 2;
	s.c = 3;
	s.d = 4;
	s.e = 5;

	Function(s);


	return 0;
}
反汇编:
1.vs2017测试结果,使用寄存器传递地址
Function(s);
008A1740  sub         esp,10h  
008A1743  mov         eax,esp  
008A1745  mov         ecx,dword ptr [s]  
008A1748  mov         dword ptr [eax],ecx  
008A174A  mov         edx,dword ptr [ebp-10h]  
008A174D  mov         dword ptr [eax+4],edx  
008A1750  mov         ecx,dword ptr [ebp-0Ch]  
008A1753  mov         dword ptr [eax+8],ecx  
008A1756  mov         edx,dword ptr [ebp-8]  
008A1759  mov         dword ptr [eax+0Ch],edx  
008A175C  call        Function (08A10A5h)  
008A1761  add         esp,10h  
2.vc++6.0测试结果,直接使用rep stos指令,在内存内复制
12:   void Function(st s)
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]

00401038   pop         edi
00401039   pop         esi
0040103A   pop         ebx
0040103B   mov         esp,ebp
0040103D   pop         ebp
0040103E   ret

结构体返回值:

# include<stdio.h>

struct st
{
	char a;
	short b;
	int c;
	int d;
	int e;

};
st Function()
{
	st s;
	s.a = 1;
	s.b = 2;
	s.c = 3;
	s.d = 4;
	s.e = 5;
	return s;
}
int main(int argc, char* argv[])
{
	st a = Function();


	return 0;
}

反汇编:
vs2017测试结果,把Function函数的返回值的地址放在eax
st a = Function();
0106171E  lea         eax,[ebp-104h]  //外层函数用来接收返回值的地址
01061724  push        eax  
01061725  call        Function (01061366h)  
0106172A  add         esp,4  
0106172D  mov         ecx,dword ptr [eax]  
0106172F  mov         dword ptr [ebp-0ECh],ecx  
01061735  mov         edx,dword ptr [eax+4]  
01061738  mov         dword ptr [ebp-0E8h],edx  
0106173E  mov         ecx,dword ptr [eax+8]  
01061741  mov         dword ptr [ebp-0E4h],ecx  
01061747  mov         edx,dword ptr [eax+0Ch]  
0106174A  mov         dword ptr [ebp-0E0h],edx  
01061750  mov         eax,dword ptr [ebp-0ECh]  
01061756  mov         dword ptr [a],eax  
01061759  mov         ecx,dword ptr [ebp-0E8h]  
0106175F  mov         dword ptr [ebp-10h],ecx  
01061762  mov         edx,dword ptr [ebp-0E4h]  
01061768  mov         dword ptr [ebp-0Ch],edx  
0106176B  mov         eax,dword ptr [ebp-0E0h]  
01061771  mov         dword ptr [ebp-8],eax 
vc++6.0测试结果,把Function函数的返回值的地址放在eax
     st a = Function();
0040D4D8   lea         eax,[ebp-30h]  //外层函数用来接收返回值的地址
0040D4DB   push        eax
0040D4DC   call        @ILT+20(Function) (00401019)
0040D4E1   add         esp,4
0040D4E4   mov         ecx,dword ptr [eax]
0040D4E6   mov         dword ptr [ebp-20h],ecx
0040D4E9   mov         edx,dword ptr [eax+4]
0040D4EC   mov         dword ptr [ebp-1Ch],edx
0040D4EF   mov         ecx,dword ptr [eax+8]
0040D4F2   mov         dword ptr [ebp-18h],ecx
0040D4F5   mov         edx,dword ptr [eax+0Ch]
0040D4F8   mov         dword ptr [ebp-14h],edx
0040D4FB   mov         eax,dword ptr [ebp-20h]
0040D4FE   mov         dword ptr [ebp-10h],eax
0040D501   mov         ecx,dword ptr [ebp-1Ch]
0040D504   mov         dword ptr [ebp-0Ch],ecx
0040D507   mov         edx,dword ptr [ebp-18h]
0040D50A   mov         dword ptr [ebp-8],edx
0040D50D   mov         eax,dword ptr [ebp-14h]
0040D510   mov         dword ptr [ebp-4],eax


<think>我们正在讨论的是通过反汇编来确认函数调用约定前置条件的方法。根据用户之前的问题,他们正在逆向一个Android APK中的native库(libstoreservicescore.so),尤其是函数`qi864985u0`,并遇到了崩溃问题。现在,用户想了解如何通过反汇编来确认调用约定前置条件。 引用文献: [1]: 关于Ghidra反汇编操作的总结,强调通过名称、数据类型注释来理解程序。 [2]: 关于x86汇编语言基础的书籍,涉及基本语言元素、伪指令、数据定义等。 [3]: 提到函数序言会改变寄存器的值,在函数开始部分准备栈寄存器。 [4]: 关于iOS汇编寄存器调用,展示了一个objc_msgSend的例子,其中RDIRSI寄存器用于传递参数。 注意:虽然引用[4]是关于iOS(x86_64架构)的,但我们当前的目标是Android(通常是ARM架构),所以需要调整到ARM的调用约定。另外,引用[2]是针对x86的,但原理类似,我们需要关注ARM汇编。 我们将通过以下步骤来确认调用约定前置条件: 1. **定位目标函数**:在反汇编工具(如Ghidra或IDA)中定位到函数`qi864985u0`。 2. **分析函数序言(Prologue)**:观察函数开头的指令,了解它如何设置栈帧保存寄存器。这有助于了解函数的栈帧大小可能保存的寄存器(即函数可能修改的寄存器)。 3. **分析参数使用**:观察函数内部如何使用传入的参数。在ARM架构中,参数通常通过寄存器R0-R3传递,如果参数多于4个,则使用栈传递。同时,注意函数内部是否访问了全局变量或其他数据结构(可能表示前置条件)。 4. **分析函数调用**:观察函数内是否调用了其他函数,并注意这些调用的参数传递方式(即调用约定),以及是否遵循ARM的AAPCS标准。 5. **分析函数结尾(Epilogue)**:结尾的指令通常恢复寄存器并返回,这有助于确认返回值的存放位置(通常是R0)。 6. **识别依赖关系**:检查函数内部是否有访问全局变量、静态数据或调用其他函数,这些可能意味着前置条件(如全局变量需要初始化,或某些函数必须先调用)。 具体步骤: 步骤1:使用Ghidra或IDA打开`libstoreservicescore.so`,并定位到函数`qi864985u0`。 - 如果函数名被混淆,可以通过地址或模式匹配(如用户已知传入-2可获取mdmd-m,那么可以搜索相关常量或字符串)来定位。 步骤2:分析函数序言(ARM架构典型序言): - 通常,函数开头会保存寄存器(如PUSH {R4-R7, LR})并设置栈指针(如SUB SP, SP, #栈大小)。这告诉我们函数使用了哪些寄存器,以及局部变量的空间。 - 例如,如果看到: PUSH {R4-R11, LR} ADD R11, SP, #0x20 SUB SP, SP, #0x50 则说明函数保存了多个寄存器,并分配了0x50字节的栈空间。 步骤3:分析参数传递: - 在ARM中,第一个参数在R0,第二个参数在R1,第三个在R2,第四个在R3。更多参数则通过栈传递(在函数序言之前由调用者压栈)。 - 在函数体内,观察R0-R3的使用情况。例如,用户之前提到这个方法可以传入dsid(一个64位整数?),那么如果是两个32位参数,可能用R0R1传递;如果是一个64位参数,则用R0R1(低32位在R0,高32位在R1)。 - 注意:如果函数是类的成员函数,那么可能会隐含一个this指针作为第一个参数(R0)。但这里函数名看起来不像成员函数,所以需要结合上下文。 步骤4:分析函数内部调用: - 如果函数调用了其他函数,观察调用前如何设置参数(即设置R0-R3栈),这可以验证调用约定(通常是AAPCS)。 - 例如,在调用另一个函数之前,将参数1放入R0,参数2放入R1,然后BL调用。 步骤5:分析函数结尾: - 典型的结尾是恢复保存的寄存器并返回。例如: POP {R4-R11, PC} 或者使用栈指针恢复(如ADD SP, SP, #栈大小)然后POP。 步骤6:识别依赖的前置条件: - 观察函数内部是否访问了全局变量(通常是通过某个地址加载数据)。如果使用了全局变量,那么这些变量需要在函数调用前被正确初始化,否则可能导致崩溃。 - 检查函数是否调用了其他函数,这些函数是否依赖某些状态(如是否初始化)。 - 特别关注函数内部是否有条件判断(如检查指针是否为NULL),这可能是崩溃的原因。 举例说明(ARM汇编伪代码): 假设我们反汇编得到: qi864985u0: PUSH {R4, R5, LR} ; 保存寄存器,LR保存返回地址 SUB SP, SP, #0x14 ; 分配栈空间 MOV R4, R0 ; 将第一个参数(dsid?)保存到R4 LDR R0, [R0] ; 如果R0是指针,则加载其指向的值 -> 这里如果是直接传递dsid(整数),那么R0就是值,这里如果直接访问就会崩溃(因为整数被当作指针) ... 如果函数期望R0是一个指针(指向某个结构体),而用户传入的是整数值(dsid),那么这里就会发生内存访问违例(SIGSEGV)。 因此,通过反汇编可以确认: - 函数的参数类型(是指针还是整数)? - 函数内部是否对参数有额外的要求(如指针非空)? 另外,如果函数内部使用了全局变量,例如: LDR R3, =g_initialized LDR R3, [R3] CMP R3, #0 BEQ error_handler 那么需要确保g_initialized已经被设置为非0。 总结:通过反汇编分析函数的汇编代码,我们可以: 1. 确定调用约定(AAPCS for ARM),参数传递方式(寄存器、栈)。 2. 确定参数的类型(通过分析函数如何使用这些参数:直接使用还是解引用?)。 3. 确定前置条件(如全局变量初始化、指针非空、某些函数已经调用等)。 接下来,针对用户的具体问题(函数传入dsid崩溃): - 通过反汇编查看函数开头对参数的使用,确认参数是整数还是指针。 - 检查函数内部是否有对参数的间接访问(如LDR指令),如果有,那么传入的dsid可能被当作指针,但实际上它是一个整数,从而导致崩溃。 建议:如果用户能提供反汇编后的函数开头几行代码(或者偏移地址),我们可以更准确地分析。 相关问题: 1. ARM架构下函数调用约定(AAPCS)的具体规则是什么? 2. 在逆向工程中,如何识别函数参数的数量类型? 3. 如何通过反汇编判断一个函数是否依赖于未初始化的全局变量? 4. 在Android native层调试中,如何捕获分析SIGSEGV信号? 注意:由于用户的目标是Android(ARM架构),所以我们的分析重点在ARM汇编,而非x86。 </think>### 通过反汇编确认调用约定前置条件的方法 #### 1. **定位函数入口与识别寄存器用法** 通过反汇编工具(如 Ghidra、IDA)定位目标函数(如 `qi864985u0`),重点分析: - **函数序言(Prologue)**: 函数开头指令(如 ARM 的 `PUSH {R4-R11, LR}` 或 x86 的 `PUSH EBP`)会保存寄存器设置栈帧[^3]。这些指令揭示了: - 需保存的寄存器(可能包含关键状态) - 栈空间大小(局部变量需求) $$ \text{示例:} \quad \text{ARM: } \texttt{PUSH \{R0-R3\}} \quad \Rightarrow \quad \text{寄存器 } R0\text{-}R3 \text{ 可能存储参数} $$ - **参数传递规则**: - **ARM 架构**:前 4 个参数通过 `R0-R3` 传递,后续参数通过栈传递(遵循 AAPCS 调用约定)[^4]。 - **x86 架构**:参数通过栈传递(如 `PUSH EAX` 压入参数),`ECX/EDX` 可能存储 `this` 指针或快速调用参数[^2]。 验证参数是否被正确解引用(如 `LDR R1, [R0]` 说明 `R0` 需为有效指针)。 --- #### 2. **分析函数依赖的前置条件** 通过反汇编代码推断隐式依赖: - **全局变量访问**: 若函数内存在类似指令: ```armasm LDR R0, =g_initialized ; 加载全局变量地址 LDR R1, [R0] ; 读取值 CMP R1, #0 ; 检查是否初始化 BEQ crash_handler ; 未初始化则跳转到崩溃处理 ``` 表明需提前初始化全局状态 `g_initialized`[^1]。 - **外部函数调用**: 若函数内调用 `BL init_core` 或 `CALL _init_subsystem`,需确保这些函数已执行[^3]。 - **寄存器依赖**: 某些寄存器在调用前需预设值(如 `R0` 作为上下文指针)。若函数开头直接使用 `R5` 而未赋值,说明调用者需提前设置[^4]。 --- #### 3. **验证调用约定与参数类型** - **参数类型检查**: - 整型参数:观察是否被当作指针解引用(如 `LDR R2, [R1]` 要求 `R1` 为地址而非原始值)。 - 结构体参数:栈偏移访问(如 `[EBP+8]`)暗示参数需连续内存布局[^2]。 - **返回值约定**: - ARM 中返回值存于 `R0`,x86 中存于 `EAX`。若函数结尾有 `MOV R0, R4`,说明结果需从 `R4` 复制到 `R0`[^3]。 --- #### 4. **动态调试辅助验证** 结合静态反汇编与动态调试: 1. 在调用函数前设置断点,检查寄存器值是否符合预期(如 `R0` 是否为有效 `dsid`)。 2. 单步执行至函数内部,观察崩溃点: - 若崩溃在 `LDR R0, [R1]`,说明 `R1` 为无效指针(参数错误)。 - 若崩溃在未初始化内存访问,需检查全局数据[^1]。 --- #### 5. **工具操作示例(Ghidra)** 1. 加载 `libstoreservicescore.so`,定位 `qi864985u0` 函数。 2. 重命名符号(如 `param_dsid → R0`)[^1]。 3. 注释关键指令: ```armasm ; 前置条件:R0 (dsid) 需为 64 位整数 LDR R1, [R0] ; 若 dsid 非指针,此处崩溃 → 参数类型错误 BL get_system_key ; 依赖外部初始化 ``` 4. 生成调用流程图,识别外部依赖节点[^1]。 --- ### 结论 通过反汇编可确认: 1. **调用约定**:参数传递方式(寄存器/栈)、返回值位置。 2. **前置条件**:全局状态初始化、外部函数调用顺序、参数有效性(指针/整型)。 用户遇到的崩溃可能原因: - `dsid` 未按指针传递(但函数要求指针) - 缺失全局初始化(如 `libstoreservicescore` 未加载配置) - 依赖函数(如密钥生成)未提前调用[^1][^3]。 --- ### 相关问题 1. ARM 与 x86 调用约定的核心区别是什么? 2. 如何通过 Ghidra 识别函数内的全局变量依赖? 3. 动态调试 Android Native 代码时如何捕获寄存器状态? 4. 函数序言(Prologue)如何影响栈帧分析崩溃调试?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值