任務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的LocalVariables2.caller調用callee
callee函數的參數入棧(由caller提供)
caller的函數地址(vm_add
), EIP入棧(代碼偏移量offset
)。備注:代碼位置=vm_add+offset3.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 ebp2.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獲取所有棧幀及對應的函數地址
代碼邏輯解析
流程圖
- 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 indexSetp10:
調用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
至此完成函數地址與函數名的關聯~