iOS堆栈信息解析(函数地址与符号关联)
任务Mach-Task
描述:一个机器无关的thread的执行环境
抽象
作用:task可以理解为一个进程,包含它的线程列表
结构体:
task_threads
task_threads将traget_task任务下的所有线程保存在act_list数组中,数组个数为act_listCnt
kern_return_t task_threads
(
task_t traget_task,
thread_act_array_t *act_list, //线程指针列表
mach_msg_type_number_t *act_listCnt //线程个数
)
thread_info
线程信息
kern_return_t thread_info
(
thread_act_t target_act,
thread_flavor_t flavor,
thread_info_t thread_info_out,
mach_msg_type_number_t *thread_info_outCnt
);
如何获取线程的堆栈数据
1.所有线程:调用内核API函数task_threads
获取指定task线程列表,即act_list
2.指定线程:调用API函数thread_info
获得对应线程信息thread_info
3.线程信息:调用thread_get_state
获得指定线程上下问信息_STRUCT_MCONTEXT
。thread_get_stateAPI两个参数随着cpu架构不同而改变。_STRUCT_MCONTEXT
结构存储当前线程栈顶指针
(sp)和最顶部的栈帧指针(frame pointer),从而获得整个线程的
调用栈`。
函数调用栈原理
指令指针
- 指令指针
IP
:指令寄存器存储,指向处理器下条等待执行的指令地址(代码内的偏移量),每次执行完 IP会增加 - 堆栈栈顶指针
SP
:堆栈指令寄存器存储,系统栈的栈顶地址 - 栈帧指针
FP
:栈帧基址指令寄存器存储,每个栈帧都有一个对应的栈帧基地址,局部变量和函数参数都可以通过FP确定,因为它们到FP的距离不会受到压栈和出栈操作影响。
为了访问函数局部变量,必须能定位每个变量。局部变量相对于堆栈指针SP
的位置在进入函数时就已确定,理论上变量可用SP
加偏移量来引用,但SP
会在函数执行期随变量的压栈和出栈而变动。尽管某些情况下编译器能跟踪栈中的变量操作以修正偏移量,但要引入可观的管理开销。而且在有些机器上(如Intel处理器),用SP
加偏移量来访问一个变量需要多条指令才能实现,由此设计了栈帧指针FP
,FP
两侧分别记录函数参数,及局部变量。
函数调用栈内部布局
栈帧:函数(运行中且未完成)占用的一块独立的连续内存区域。
函数调用通常是嵌套的,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
编译器利用栈帧,使得函数参数和函数中局部变量的分配与释放对程序员透明。编译器将控制权移交函数本身之前,插入特定代码将函数参数压入栈帧中,并分配足够的内存空间用于存放函数中的局部变量。使用栈帧的一个好处是使得递归变为可能,因为对函数的每次递归调用,都会分配给该函数一个新的栈帧,这样就巧妙地隔离当前调用与上次调用。
栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
函数出入栈过程
- BP栈帧指针地址:间隔被调用函数(局部变量内存空间)和调用函数(被调函数参数,调用函数地址,指令指针)
- BP栈帧指针值:上一个栈帧的地址值,便于被调函数释放后,回到调用函数
- BP栈帧入栈时机:函数被调用,申请内存空间来存储前一个栈帧的地址值
从图中可以看出,函数调用时入栈顺序为:
实参N-1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1-N 。
注意:
内存地址降序
函数定义
- caller(主调函数,紫色)
- callee(被调函数,蓝色)
入栈过程
-
1.caller未调用callee,内存分布如下:
EBP
:caller EBP
ESP
:caller的LocalVariables -
2.caller调用callee
callee函数的参数入栈(由caller提供)
caller的函数地址(vm_add
), EIP入栈(代码偏移量offset
)。备注:代码位置=vm_add+offset -
3.callee栈帧指针入栈
申请栈帧指针空间
存储caller的栈帧指针地址 -
4.申请callee局部变量空间
为局部变量申请足够的内存空间
Local Variable#1,Local Variable#2,Local Variable#3...Local Variable#n
EBP
:callee的EBP
ESP
:Local Variable#n
出栈过程
-
1.callee调用完毕
callee局部变量空间释放
EBP
:callee ebp -> caller ebb
ESP
:caller ebp -
2.caller函数执行复原
代码执行复原:ip+return address =代码位置
callee函数空间释放:Argumne #1,Argumne #2,...,Argumne #1n
EBP
:caller ebb
ESP
:caller Load Variables
函数调用地址获取
获取thread
API函数task_thread获取线程数组地址
及线程个数
API函数task_thread声明
kern_return_t task_threads
(
task_t traget_task,
thread_act_array_t *act_list, //线程指针列表
mach_msg_type_number_t *act_listCnt //线程个数
)
使用代码
thread_act_array_t threads;
mach_msg_type_number_t thread_count=0;
task_threads(mach_task_self(), &thrads, &thread_count);
thread的内存上下文
API函数thread_get_state获取内存上下文,上下文信息存储在_struct_mcontext结构体内
kern_return_t thread_get_state
(
thread_act_t target_act, //thread
thread_state_flavor_t flavor,
thread_state_t old_state,
mach_msg_type_number_t *old_stateCnt
);
备注:
target_act和old_stateCnt配套使用,与cpu类型相关
使用代码
bool fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT * machineContext) {
mach_msg_type_number_t state_count = LSL_THREAD_STATE_COUNT;
kern_return_t kr = thread_get_state(thread, LSL_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
return (kr == KERN_SUCCESS);
}
thread_get_state传入thread,_STRUCT_MCONTEXT->__ss(寄存器指针结构体)
,以及cpu相关常量(target_act,old_stateCnt),来实现_STRUCT_MCONTEXT赋值
堆栈指针获取
_STRUCT_MCONTEXT结构体获取堆栈指针
如x86_64为_STRUCT_MCONTEXT->__ss结构体如下
#define DETAG_INSTRUCTION_ADDRESS(A) (A)
#define LSL_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT //thread_get_state函数参数
#define LSL_THREAD_STATE x86_THREAD_STATE64 //thread_get_state函数参数
#define LSL_FRAME_POINTER __rbp
#define LSL_STACK_POINTER __rsp
#define LSL_INSTRUCTION_ADDRESS __rip
指令指针
_STRUCT_MCONTEXT->__ss.LSL_INSTRUCTION_ADDRESS //rip 指令指针
栈顶指针
_STRUCT_MCONTEXT->__ss.LSL_STACK_POINTER //bsp 栈顶指针
栈帧指针
_STRUCT_MCONTEXT->__ss.LSL_FRAME_POINTER //rbp 栈帧指针
栈帧结构体
栈帧结构体StackFrameEntry
typedef struct StackFrameEntry{
const struct StackFrameEntry *const previous; //前一个栈帧地址
const uintptr_t return_address; //栈帧的函数返回地址
} StackFrameEntry;
首个栈帧结构体赋值
API函数vm_read_overwrite
kern_return_t vm_read_overwrite
(
vm_map_t target_task, //task任务
vm_address_t address, //栈帧指针FP
vm_size_t size, //结构体大小 sizeof(StackFrameEntry)
vm_address_t data, //结构体指针StackFrameEntry
vm_size_t *outsize //赋值大小
);
使用代码
//参数src:栈帧指针
//参数dst:StackFrameEntry实例指针
//参数numBytes:StackFrameEntry结构体大小
kern_return_t lsl_mach_copyMem(const void * src, const void * dst, const size_t numBytes) {
vm_size_t bytesCopied = 0;
// 调用api函数,根据栈帧指针获取该栈帧对应的函数地址
return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}
函数地址
参考上一步,完成首个栈帧结构体赋值后
1.通过栈帧结构体StackFrameEntry->previous,遍历所有栈帧
2.API函数vm_read_overwrite对栈帧结构体赋值,获取当前栈帧函数
伪代码
//循环遍历,停止条件MAX_FRAME_NUMBER栈帧个数
for (; idx < MAX_FRAME_NUMBER; idx++) {
栈帧函数赋值
backtraceBuffer[idx] = frame.return_address;
if (backtraceBuffer[idx] == FAILED_UINT_PTR_ADDRESS ||
frame.previous == NULL ||
// 根据当前的栈帧的previous,获取前一个栈帧地址
lsl_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
break;
}
线程函数地址获取小结
- 1.找到目标thread,方法:API函数
task_threads
- 2.获得thread的内存上下文_STRUCT_CONTEXT,方法:API函数
thread_get_state
- 3.获取指针栈帧结构体
_STRUCT_CONTEXT.
_ss,解析得到对应指令指针_STRUCT_CONTEXT._ss.ip;首次
个栈帧指针_STRUCT_CONTEXT._ss.bp;栈顶指针_STRUCT_CONTEXT._ss.sp -
首个
栈帧结构体赋值,方法:API函数vm_read_overwrite
(_STRUCT_CONTEXT._ss.bp...),完成首个
栈帧结构体赋值StackFrameEntry
-
- 遍历StackFrameEntry获取所有栈帧及对应的函数地址
代码逻辑解析
流程图
image.png- Setp1:
调用API函数task_threads
,获取线程数组栈帧threads,线程个数thread_count
task_threads(mach_task_self(), &threads, &thread_count)
- Setp2:
调用API函数thread_get_state
,实例化结构体STRUCT_MCONTEXT
,STRUCT_MCONTEXT->__ss包含栈帧指针fp,指令指针ip,栈顶指针sp
//thread:线程
//LSL_THREAD_STATE:cpu相关的定量
//machineContext->__ss:设备上下文,__ss结构体存储了`fp`,ip,sp
//state_count:cpu相关的定量
thread_get_state(thread, LSL_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count)
- Setp3:
调用API函数vm_read_overwrite
,实例化StackFrameEntry
结构体,StackFrameEntry存储首个栈帧的函数地址
,以及前一个栈帧地址
从而通过遍历
堆栈所有函数地址的获取
typedef struct StackFrameEntry{
// 前一个栈帧地址
const struct StackFrameEntry * const previous;
// 函数地址
const uintptr_t return_address;
} StackFrameEntry;
//mach_task_self:task对象
//src:fp栈帧指针
//numBytes:sizeof(StackFrameEntry)
//dst:StackFrameEntry指针
//bytesCopied://cpye字节大小
vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied)
-
Setp4:
遍历StackFrameEntry(遍历条件StackFrameEntry.previous
),来获取堆栈所有栈帧地址,及函数地址(add)并存储在函数地址数组backTrackBuffer
。 -
Setp5:
获得函数的实现地址
,由于函数地址无法进行阅读,需要通过符号表(nlist)来解析为函数名
(Setp6-Setp15操作目标),从而进行程序定位。 -
Setp6:
调用API函数_dyld_image_count(void) ,获取images文件总数,即mach-o文件总数,Setp6-Setp9遍历获取mach-o target index(目标mach-o镜像文件)。 -
Setp7:
调用API函数_dyld_get_image_header(imageIndex)
获取mach-o文件的header对象,header对象存储load command个数及大小;
调用API函数_dyld_get_image_vmadd_slide(imageIndex)
的mach-o文件的随机内存地址偏移量 -
Setp8:
补充
函数地址:add
,函数真实的实现地址
函数虚拟地址:vm_add
,
ALSR:slide
函数虚拟地址加载到进程内存的随机偏移量,每个mach-o的slide各不相同
关系:vm_add + slide = add
已知参数
:add,slide因此通过关系换算得到vm_add
-
Setp9:
image index:函数对应的mach-o镜像文件image索引index
遍历:遍历mach-o下所有loadCommand(LC_SEGMENT),循环条件header->ncmds(load command个数)。
目标:函数地址对应的mach-o镜像文件image。
查询条件:vm_add=[image(index).segment(i).vmadd
, image(index).segment(i).vmadd
+image(index).segment(i).vmsize
],其中index=image index,i=cmd index -
Setp10:
调用API函数_dyld_get_image_vmaddr_slide(index),获取目标image的slide
用来换算基址。不同的mach-o的slide不同 -
Setp11:
获得函数对应的mach-o的镜像image(index)文件后,计算程序链接基址,从而获取符号表地址symbolTab_Add,字符串表地址strTab_Add。
base_add
=segmet(LINKEDIR).vmadd
-segment(LINKEDIT).fileoff
+slide
函数对应的镜像文件image(index),遍历loadcommadn,获得cmd.segname=LINKEDIT的segment,提取vmadd
(虚拟地址),fileoff
(文件偏移量) -
Setp12:
获得符号表地址
symbolTab_add
:符号表地址,一块连续的地址来存储mach-o所有的函数符号,存储结构为nlis
base_add
:程序链接时基址,通过LINKEDIT计算得到
symoff
:符号表偏移地址,存储在LC_SYMTAB的cmd中,symoff为相对基址的偏移量
关系:symbolTab_add = base_add + symoff
-
Setp13:
获得字符串表地址
strTab_add:符号表地址,一块连续的地址来存储mach-o所有的字符串指针
base_add:程序链接时基址,通过LINKEDIT计算得到
stroff:符号表偏移地址,存储在LC_SYMTAB的cmd中,stroff为相对基址的偏移量
关系:strTab_add = base_add + stroff
-
Setp14:
符号表结构体nlist
// 位于系统库 头文件中 struct nlist {
union {
uint32_t n_strx; //符号名在字符串表中的偏移量
} n_un;
uint8_t n_type;
uint8_t n_sect;
int16_t n_desc;
uint32_t n_value; //符号在内存中的地址,类似于函数虚拟地址指针
};
符号表以nlist的结构体连续存储mach-o文件下所有函数符号,nlist结构体将函数虚拟地址,与函数名进行关联。
-
Setp15:
符号结构体nlist关联了函数虚拟地址和函数名(n_vaule函数虚拟地址,n_um_strx字符串表偏移量),目前已知函数地址,因此可以遍历所有的nlist获得对应的n_um_strx。
函数虚拟地址vm_add
: vm_add = add - slide
符号表注册函数虚拟地址n_value
:nlist(index).n_value
index遍历条件:vm_add >= n_value && min(vm_add - n_value)
,满足上述条件的符号index即为函数对应的nlist(index) -
Setp16:
获得函数对应的符号表索引后,得到函数名起始地址nlist(inde).n_um.n_strx + strTab_add
至此完成函数地址与函数名的关联~