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加偏移量來訪問一個變量需要多條指令才能實現,由此設計了棧幀指針FPFP兩側分別記錄函數參數,及局部變量。

函數調用棧內部布局
棧幀:函數(運行中且未完成)占用的一塊獨立的連續內存區域。
函數調用通常是嵌套的,當調用函數時邏輯棧幀被壓入堆棧, 當函數返回時邏輯棧幀被從堆棧中彈出。棧幀存放著函數參數,局部變量及恢復前一棧幀所需要的數據等。

編譯器利用棧幀,使得函數參數和函數中局部變量的分配與釋放對程序員透明。編譯器將控制權移交函數本身之前,插入特定代碼將函數參數壓入棧幀中,并分配足夠的內存空間用于存放函數中的局部變量。使用棧幀的一個好處是使得遞歸變為可能,因為對函數的每次遞歸調用,都會分配給該函數一個新的棧幀,這樣就巧妙地隔離當前調用與上次調用。

棧幀的邊界由棧幀基地址指針EBP和堆棧指針ESP界定(指針存放在相應寄存器中)。EBP指向當前棧幀底部(高地址),在當前棧幀內位置固定;ESP指向當前棧幀頂部(低地址),當程序執行時ESP會隨著數據的入棧和出棧而移動。因此函數中對大部分數據的訪問都基于EBP進行。

函數出入棧過程

  • BP棧幀指針地址:間隔被調用函數(局部變量內存空間)和調用函數(被調函數參數,調用函數地址,指令指針)
  • BP棧幀指針值:上一個棧幀的地址值,便于被調函數釋放后,回到調用函數
  • BP棧幀入棧時機:函數被調用,申請內存空間來存儲前一個棧幀的地址值

函數調用棧內部布局.png

從圖中可以看出,函數調用時入棧順序為:
實參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
    1. 首個棧幀結構體賦值,方法:API函數vm_read_overwrite(_STRUCT_CONTEXT._ss.bp...),完成首個棧幀結構體賦值StackFrameEntry
    1. 遍歷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

至此完成函數地址與函數名的關聯~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容