iOS堆棧信息解析(函數(shù)地址與符號(hào)關(guān)聯(lián))

任務(wù)Mach-Task

描述:一個(gè)機(jī)器無(wú)關(guān)的thread的執(zhí)行環(huán)境抽象
作用:task可以理解為一個(gè)進(jìn)程,包含它的線程列表
結(jié)構(gòu)體:
task_threads
task_threads將traget_task任務(wù)下的所有線程保存在act_list數(shù)組中,數(shù)組個(gè)數(shù)為act_listCnt

kern_return_t task_threads
(
  task_t traget_task,
  thread_act_array_t *act_list,                     //線程指針列表
  mach_msg_type_number_t *act_listCnt  //線程個(gè)數(shù)
)

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
);

如何獲取線程的堆棧數(shù)據(jù)
1.所有線程:調(diào)用內(nèi)核API函數(shù)task_threads獲取指定task線程列表,即act_list
2.指定線程:調(diào)用API函數(shù)thread_info獲得對(duì)應(yīng)線程信息thread_info
3.線程信息:調(diào)用thread_get_state獲得指定線程上下問(wèn)信息_STRUCT_MCONTEXT。thread_get_stateAPI兩個(gè)參數(shù)隨著cpu架構(gòu)不同而改變。_STRUCT_MCONTEXT結(jié)構(gòu)存儲(chǔ)當(dāng)前線程棧頂指針(sp)和最頂部的棧幀指針(frame pointer),從而獲得整個(gè)線程的調(diào)用棧`。

函數(shù)調(diào)用棧原理

指令指針

  • 指令指針IP:指令寄存器存儲(chǔ),指向處理器下條等待執(zhí)行的指令地址(代碼內(nèi)的偏移量),每次執(zhí)行完 IP會(huì)增加
  • 堆棧棧頂指針SP:堆棧指令寄存器存儲(chǔ),系統(tǒng)棧的棧頂?shù)刂?/li>
  • 棧幀指針FP:棧幀基址指令寄存器存儲(chǔ),每個(gè)棧幀都有一個(gè)對(duì)應(yīng)的棧幀基地址,局部變量和函數(shù)參數(shù)都可以通過(guò)FP確定,因?yàn)樗鼈兊紽P的距離不會(huì)受到壓棧和出棧操作影響。

為了訪問(wèn)函數(shù)局部變量,必須能定位每個(gè)變量。局部變量相對(duì)于堆棧指針SP的位置在進(jìn)入函數(shù)時(shí)就已確定,理論上變量可用SP加偏移量來(lái)引用,但SP會(huì)在函數(shù)執(zhí)行期隨變量的壓棧和出棧而變動(dòng)。盡管某些情況下編譯器能跟蹤棧中的變量操作以修正偏移量,但要引入可觀的管理開(kāi)銷。而且在有些機(jī)器上(如Intel處理器),用SP加偏移量來(lái)訪問(wèn)一個(gè)變量需要多條指令才能實(shí)現(xiàn),由此設(shè)計(jì)了棧幀指針FP,FP兩側(cè)分別記錄函數(shù)參數(shù),及局部變量。

函數(shù)調(diào)用棧內(nèi)部布局
棧幀:函數(shù)(運(yùn)行中且未完成)占用的一塊獨(dú)立的連續(xù)內(nèi)存區(qū)域。
函數(shù)調(diào)用通常是嵌套的,當(dāng)調(diào)用函數(shù)時(shí)邏輯棧幀被壓入堆棧, 當(dāng)函數(shù)返回時(shí)邏輯棧幀被從堆棧中彈出。棧幀存放著函數(shù)參數(shù),局部變量及恢復(fù)前一棧幀所需要的數(shù)據(jù)等。

編譯器利用棧幀,使得函數(shù)參數(shù)和函數(shù)中局部變量的分配與釋放對(duì)程序員透明。編譯器將控制權(quán)移交函數(shù)本身之前,插入特定代碼將函數(shù)參數(shù)壓入棧幀中,并分配足夠的內(nèi)存空間用于存放函數(shù)中的局部變量。使用棧幀的一個(gè)好處是使得遞歸變?yōu)榭赡?,因?yàn)閷?duì)函數(shù)的每次遞歸調(diào)用,都會(huì)分配給該函數(shù)一個(gè)新的棧幀,這樣就巧妙地隔離當(dāng)前調(diào)用與上次調(diào)用。

棧幀的邊界由棧幀基地址指針EBP和堆棧指針ESP界定(指針存放在相應(yīng)寄存器中)。EBP指向當(dāng)前棧幀底部(高地址),在當(dāng)前棧幀內(nèi)位置固定;ESP指向當(dāng)前棧幀頂部(低地址),當(dāng)程序執(zhí)行時(shí)ESP會(huì)隨著數(shù)據(jù)的入棧和出棧而移動(dòng)。因此函數(shù)中對(duì)大部分?jǐn)?shù)據(jù)的訪問(wèn)都基于EBP進(jìn)行。

函數(shù)出入棧過(guò)程

  • BP棧幀指針地址:間隔被調(diào)用函數(shù)(局部變量?jī)?nèi)存空間)和調(diào)用函數(shù)(被調(diào)函數(shù)參數(shù),調(diào)用函數(shù)地址,指令指針)
  • BP棧幀指針值:上一個(gè)棧幀的地址值,便于被調(diào)函數(shù)釋放后,回到調(diào)用函數(shù)
  • BP棧幀入棧時(shí)機(jī):函數(shù)被調(diào)用,申請(qǐng)內(nèi)存空間來(lái)存儲(chǔ)前一個(gè)棧幀的地址值

函數(shù)調(diào)用棧內(nèi)部布局.png

從圖中可以看出,函數(shù)調(diào)用時(shí)入棧順序?yàn)椋?br> 實(shí)參N-1→主調(diào)函數(shù)返回地址→主調(diào)函數(shù)幀基指針EBP→被調(diào)函數(shù)局部變量1-N 。
注意:內(nèi)存地址降序

函數(shù)定義

  • caller(主調(diào)函數(shù),紫色)
  • callee(被調(diào)函數(shù),藍(lán)色)

入棧過(guò)程

  • 1.caller未調(diào)用callee,內(nèi)存分布如下:
    EBP:caller EBP
    ESP:caller的LocalVariables

  • 2.caller調(diào)用callee
    callee函數(shù)的參數(shù)入棧(由caller提供)
    caller的函數(shù)地址(vm_add), EIP入棧(代碼偏移量offset)。備注:代碼位置=vm_add+offset

  • 3.callee棧幀指針入棧
    申請(qǐng)棧幀指針空間
    存儲(chǔ)caller的棧幀指針地址

  • 4.申請(qǐng)callee局部變量空間
    為局部變量申請(qǐng)足夠的內(nèi)存空間
    Local Variable#1,Local Variable#2,Local Variable#3...Local Variable#n
    EBP:callee的EBP
    ESP:Local Variable#n

出棧過(guò)程

  • 1.callee調(diào)用完畢
    callee局部變量空間釋放
    EBP:callee ebp -> caller ebb
    ESP:caller ebp

  • 2.caller函數(shù)執(zhí)行復(fù)原
    代碼執(zhí)行復(fù)原:ip+return address = 代碼位置
    callee函數(shù)空間釋放:Argumne #1,Argumne #2,...,Argumne #1n
    EBP:caller ebb
    ESP:caller Load Variables

函數(shù)調(diào)用地址獲取

獲取thread
API函數(shù)task_thread獲取線程數(shù)組地址線程個(gè)數(shù)
API函數(shù)task_thread聲明

kern_return_t task_threads
(
  task_t traget_task,
  thread_act_array_t *act_list,   //線程指針列表
  mach_msg_type_number_t *act_listCnt  //線程個(gè)數(shù)
)

使用代碼

thread_act_array_t threads;
mach_msg_type_number_t thread_count=0;
task_threads(mach_task_self(),  &thrads, &thread_count);

thread的內(nèi)存上下文
API函數(shù)thread_get_state獲取內(nèi)存上下文,上下文信息存儲(chǔ)在_struct_mcontext結(jié)構(gòu)體內(nèi)

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類型相關(guān)

使用代碼

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(寄存器指針結(jié)構(gòu)體),以及cpu相關(guān)常量(target_act,old_stateCnt),來(lái)實(shí)現(xiàn)_STRUCT_MCONTEXT賦值

堆棧指針獲取
_STRUCT_MCONTEXT結(jié)構(gòu)體獲取堆棧指針
如x86_64為_(kāi)STRUCT_MCONTEXT->__ss結(jié)構(gòu)體如下

#define DETAG_INSTRUCTION_ADDRESS(A) (A)
#define LSL_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT //thread_get_state函數(shù)參數(shù)
#define LSL_THREAD_STATE x86_THREAD_STATE64 //thread_get_state函數(shù)參數(shù)
#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 棧幀指針

棧幀結(jié)構(gòu)體
棧幀結(jié)構(gòu)體StackFrameEntry

typedef struct StackFrameEntry{
    const struct StackFrameEntry *const previous;  //前一個(gè)棧幀地址
    const uintptr_t return_address;  //棧幀的函數(shù)返回地址
} StackFrameEntry;

首個(gè)棧幀結(jié)構(gòu)體賦值
API函數(shù)vm_read_overwrite

kern_return_t vm_read_overwrite
(
    vm_map_t target_task,  //task任務(wù)
    vm_address_t address,  //棧幀指針FP
    vm_size_t size,  //結(jié)構(gòu)體大小 sizeof(StackFrameEntry)
    vm_address_t data,  //結(jié)構(gòu)體指針StackFrameEntry
    vm_size_t *outsize  //賦值大小
);

使用代碼


//參數(shù)src:棧幀指針
//參數(shù)dst:StackFrameEntry實(shí)例指針
//參數(shù)numBytes:StackFrameEntry結(jié)構(gòu)體大小
kern_return_t lsl_mach_copyMem(const void * src, const void * dst, const size_t numBytes) {
    vm_size_t bytesCopied = 0;
//   調(diào)用api函數(shù),根據(jù)棧幀指針獲取該棧幀對(duì)應(yīng)的函數(shù)地址
    return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}

函數(shù)地址
參考上一步,完成首個(gè)棧幀結(jié)構(gòu)體賦值后
1.通過(guò)棧幀結(jié)構(gòu)體StackFrameEntry->previous,遍歷所有棧幀
2.API函數(shù)vm_read_overwrite對(duì)棧幀結(jié)構(gòu)體賦值,獲取當(dāng)前棧幀函數(shù)
偽代碼

//循環(huán)遍歷,停止條件MAX_FRAME_NUMBER棧幀個(gè)數(shù)
    for (; idx < MAX_FRAME_NUMBER; idx++) {
 棧幀函數(shù)賦值
        backtraceBuffer[idx] = frame.return_address;
        
        if (backtraceBuffer[idx] == FAILED_UINT_PTR_ADDRESS ||
            frame.previous == NULL ||
//        根據(jù)當(dāng)前的棧幀的previous,獲取前一個(gè)棧幀地址
            lsl_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            break;
        }

線程函數(shù)地址獲取小結(jié)

  • 1.找到目標(biāo)thread,方法:API函數(shù)task_threads
  • 2.獲得thread的內(nèi)存上下文_STRUCT_CONTEXT,方法:API函數(shù)thread_get_state
  • 3.獲取指針棧幀結(jié)構(gòu)體_STRUCT_CONTEXT._ss,解析得到對(duì)應(yīng)指令指針_STRUCT_CONTEXT._ss.ip;首次個(gè)棧幀指針_STRUCT_CONTEXT._ss.bp;棧頂指針_STRUCT_CONTEXT._ss.sp
    1. 首個(gè)棧幀結(jié)構(gòu)體賦值,方法:API函數(shù)vm_read_overwrite(_STRUCT_CONTEXT._ss.bp...),完成首個(gè)棧幀結(jié)構(gòu)體賦值StackFrameEntry
    1. 遍歷StackFrameEntry獲取所有棧幀及對(duì)應(yīng)的函數(shù)地址

代碼邏輯解析

流程圖

image.png
  • Setp1:
    調(diào)用API函數(shù)task_threads,獲取線程數(shù)組棧幀threads,線程個(gè)數(shù)thread_count
task_threads(mach_task_self(), &threads, &thread_count)
  • Setp2:
    調(diào)用API函數(shù)thread_get_state,實(shí)例化結(jié)構(gòu)體STRUCT_MCONTEXT,STRUCT_MCONTEXT->__ss包含棧幀指針fp,指令指針ip,棧頂指針sp
//thread:線程
//LSL_THREAD_STATE:cpu相關(guān)的定量
//machineContext->__ss:設(shè)備上下文,__ss結(jié)構(gòu)體存儲(chǔ)了`fp`,ip,sp
//state_count:cpu相關(guān)的定量
thread_get_state(thread, LSL_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count)
  • Setp3:
    調(diào)用API函數(shù)vm_read_overwrite,實(shí)例化StackFrameEntry結(jié)構(gòu)體,StackFrameEntry存儲(chǔ)首個(gè)棧幀的函數(shù)地址,以及前一個(gè)棧幀地址從而通過(guò)遍歷堆棧所有函數(shù)地址的獲取
typedef struct StackFrameEntry{
    //    前一個(gè)棧幀地址
    const struct StackFrameEntry * const previous;
    //    函數(shù)地址
    const uintptr_t return_address;
} StackFrameEntry;

//mach_task_self:task對(duì)象
//src:fp棧幀指針
//numBytes:sizeof(StackFrameEntry)
//dst:StackFrameEntry指針
//bytesCopied://cpye字節(jié)大小
vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied)
  • Setp4:
    遍歷StackFrameEntry(遍歷條件StackFrameEntry.previous),來(lái)獲取堆棧所有棧幀地址,及函數(shù)地址(add)并存儲(chǔ)在函數(shù)地址數(shù)組backTrackBuffer。

  • Setp5:
    獲得函數(shù)的實(shí)現(xiàn)地址,由于函數(shù)地址無(wú)法進(jìn)行閱讀,需要通過(guò)符號(hào)表(nlist)來(lái)解析為函數(shù)名(Setp6-Setp15操作目標(biāo)),從而進(jìn)行程序定位。

  • Setp6:
    調(diào)用API函數(shù)_dyld_image_count(void) ,獲取images文件總數(shù),即mach-o文件總數(shù),Setp6-Setp9遍歷獲取mach-o target index(目標(biāo)mach-o鏡像文件)。

  • Setp7:
    調(diào)用API函數(shù)_dyld_get_image_header(imageIndex)獲取mach-o文件的header對(duì)象,header對(duì)象存儲(chǔ)load command個(gè)數(shù)及大小;
    調(diào)用API函數(shù)_dyld_get_image_vmadd_slide(imageIndex)的mach-o文件的隨機(jī)內(nèi)存地址偏移量

  • Setp8:
    補(bǔ)充
    函數(shù)地址:add,函數(shù)真實(shí)的實(shí)現(xiàn)地址
    函數(shù)虛擬地址:vm_add,
    ALSR:slide函數(shù)虛擬地址加載到進(jìn)程內(nèi)存的隨機(jī)偏移量,每個(gè)mach-o的slide各不相同
    關(guān)系:vm_add + slide = add
    已知參數(shù):add,slide因此通過(guò)關(guān)系換算得到vm_add

  • Setp9:
    image index:函數(shù)對(duì)應(yīng)的mach-o鏡像文件image索引index
    遍歷:遍歷mach-o下所有l(wèi)oadCommand(LC_SEGMENT),循環(huán)條件header->ncmds(load command個(gè)數(shù))。
    目標(biāo):函數(shù)地址對(duì)應(yīng)的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:
    調(diào)用API函數(shù)_dyld_get_image_vmaddr_slide(index),獲取目標(biāo)image的slide用來(lái)?yè)Q算基址。不同的mach-o的slide不同

  • Setp11:
    獲得函數(shù)對(duì)應(yīng)的mach-o的鏡像image(index)文件后,計(jì)算程序鏈接基址,從而獲取符號(hào)表地址symbolTab_Add,字符串表地址strTab_Add。
    base_add = segmet(LINKEDIR).vmadd - segment(LINKEDIT).fileoff + slide
    函數(shù)對(duì)應(yīng)的鏡像文件image(index),遍歷loadcommadn,獲得cmd.segname=LINKEDIT的segment,提取vmadd(虛擬地址),fileoff(文件偏移量)

  • Setp12:
    獲得符號(hào)表地址
    symbolTab_add:符號(hào)表地址,一塊連續(xù)的地址來(lái)存儲(chǔ)mach-o所有的函數(shù)符號(hào),存儲(chǔ)結(jié)構(gòu)為nlis
    base_add:程序鏈接時(shí)基址,通過(guò)LINKEDIT計(jì)算得到
    symoff:符號(hào)表偏移地址,存儲(chǔ)在LC_SYMTAB的cmd中,symoff為相對(duì)基址的偏移量
    關(guān)系:symbolTab_add = base_add + symoff

  • Setp13:
    獲得字符串表地址
    strTab_add:符號(hào)表地址,一塊連續(xù)的地址來(lái)存儲(chǔ)mach-o所有的字符串指針base_add:程序鏈接時(shí)基址,通過(guò)LINKEDIT計(jì)算得到stroff:符號(hào)表偏移地址,存儲(chǔ)在LC_SYMTAB的cmd中,stroff為相對(duì)基址的偏移量
    關(guān)系:strTab_add = base_add + stroff

  • Setp14:
    符號(hào)表結(jié)構(gòu)體nlist

// 位于系統(tǒng)庫(kù) 頭文件中 struct nlist {
  union {
     uint32_t n_strx;  //符號(hào)名在字符串表中的偏移量
  } n_un;
  uint8_t n_type;
  uint8_t n_sect;
  int16_t n_desc; 
  uint32_t n_value; //符號(hào)在內(nèi)存中的地址,類似于函數(shù)虛擬地址指針   
};

符號(hào)表以nlist的結(jié)構(gòu)體連續(xù)存儲(chǔ)mach-o文件下所有函數(shù)符號(hào),nlist結(jié)構(gòu)體將函數(shù)虛擬地址,與函數(shù)名進(jìn)行關(guān)聯(lián)。

  • Setp15:
    符號(hào)結(jié)構(gòu)體nlist關(guān)聯(lián)了函數(shù)虛擬地址和函數(shù)名(n_vaule函數(shù)虛擬地址,n_um_strx字符串表偏移量),目前已知函數(shù)地址,因此可以遍歷所有的nlist獲得對(duì)應(yīng)的n_um_strx。
    函數(shù)虛擬地址vm_add: vm_add = add - slide
    符號(hào)表注冊(cè)函數(shù)虛擬地址n_value:nlist(index).n_value
    index遍歷條件:vm_add >= n_value && min(vm_add - n_value),滿足上述條件的符號(hào)index即為函數(shù)對(duì)應(yīng)的nlist(index)

  • Setp16:
    獲得函數(shù)對(duì)應(yīng)的符號(hào)表索引后,得到函數(shù)名起始地址nlist(inde).n_um.n_strx + strTab_add

至此完成函數(shù)地址與函數(shù)名的關(guān)聯(lián)~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容