內(nèi)存分區(qū)
邏輯上劃分(編譯器劃分)
- 代碼區(qū):存放代碼,可讀可執(zhí)行
- 棧區(qū):參數(shù)、局部變量、臨時數(shù)據(jù)??啥炭蓪?/li>
- 堆區(qū):動態(tài)申請??勺x可寫
- 全局變量:可讀可寫
- 常量:只讀
全局變量和常量
int g = 12;
int func(int a, int b) {
printf("test");
int c = a + g + b;
return c;
}
int main(int argc, char * argv[]) {
func(1, 2);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
對應(yīng)的匯編:

可以看到
printf函數(shù)的參數(shù)來源為:
0x104492140 <+20>: adrp x0, 1
0x104492144 <+24>: add x0, x0, #0xf81 ; =0xf81
X0存儲的是一個地址,為字符串常量區(qū)。那么以上兩條指令是怎么獲得0x0000000104493f81這個地址的?
-
adrp:Address Page 內(nèi)存地址以頁尋址。 -
0x104492140 <+20>: adrp x0, 1:定位到某一頁數(shù)據(jù)的開始(文件起始位置)
1.將1的值左移12位變成0x1000。
2.當(dāng)前pc的值低12位清零。0x104492140 -> 0x104492000。
3.0x104492000 + 0x1000得到0x104493000。相當(dāng)于pc后3位置為0,第4位加上x0后跟的值。 -
0x104492144 <+24>: add x0, x0, #0xf81:偏移地址(當(dāng)前代碼偏移)
0x104493000 + 0xf81得到0x104493f81。
這樣就得到了常量區(qū)字符串的地址了。
0x104493000尾數(shù)為000意味著000~fff -> 0~4095大小為4096也就是4k。也就是定位到某一頁數(shù)據(jù)的開始。mac中pagesize 4k iOS中 pagesize 16k。這里是兼容的4k * 4 = 16k。
這里以pc寄存器為參照,也就是當(dāng)前代碼段地址為參照。
繼續(xù)執(zhí)行:

通過上面的規(guī)則可以計算出
x9最終的值為0x1044955d8也就是全局變量g的值。
??全局變量和常量都是通過一個 基地址 + 偏移 獲取。
匯編還原高級代碼
先編譯要還原的工程,進入.app找到macho文件并拖入Hopper中

Hopper分析完成后搜索要分析的函數(shù)

通過匯編我們可以還原高級語言的行為。
比如剛才func分析出來匯編代碼為:
_func:
000000010000612c sub sp, sp, #0x20 ; CODE XREF=_main+32
0000000100006130 stp x29, x30, [sp, #0x10]
0000000100006134 add x29, sp, #0x10
0000000100006138 stur w0, [x29, var_4]
000000010000613c str w1, [sp, #0x10 + var_8]
0000000100006140 adrp x0, #0x100007000 ; 0x100007f81@PAGE, argument "format" for method imp___stubs__printf
0000000100006144 add x0, x0, #0xf81 ; 0x100007f81@PAGEOFF, "test"
0000000100006148 bl imp___stubs__printf ; printf
000000010000614c ldur w8, [x29, var_4]
0000000100006150 adrp x9, #0x100009000 ; 0x1000095d8@PAGE
0000000100006154 add x9, x9, #0x5d8 ; 0x1000095d8@PAGEOFF, _g
0000000100006158 ldr w10, [x9] ; _g
000000010000615c add w8, w8, w10
0000000100006160 ldr w10, [sp, #0x10 + var_8]
0000000100006164 add w8, w8, w10
0000000100006168 str w8, [sp, #0x10 + var_C]
000000010000616c ldr w8, [sp, #0x10 + var_C]
0000000100006170 mov x0, x8
0000000100006174 ldp x29, x30, [sp, #0x10]
0000000100006178 add sp, sp, #0x20
000000010000617c ret
; endp
可以通過MachOView查找0000000100007f81:

00000001000095d8:

分析完代碼如下:
int global_g = 12;
int func2(int a, int b) {
//_func:
//000000010000612c sub sp, sp, #0x20 ; CODE XREF=_main+32
//0000000100006130 stp x29, x30, [sp, #0x10]
//0000000100006134 add x29, sp, #0x10
//4字節(jié)參數(shù)2個
//0000000100006138 stur w0, [x29, var_4]
//000000010000613c str w1, [sp, #0x10 + var_8]
//這里已經(jīng)算好了adrp的結(jié)果 等價于 0000000100006140 adrp x0, #0x1
//0000000100006140 adrp x0, #0x100007000 ; 0x100007f81@PAGE, argument "format" for method imp___stubs__printf
//0000000100006144 add x0, x0, #0xf81 ; 0x100007f81@PAGEOFF, "test"
//這里可以算出地址為 0000000100007f81,在 MachOView 查找為 test
//0000000100006148 bl imp___stubs__printf ; printf
printf("test");
//000000010000614c ldur w8, [x29, var_4]
int w8 = a;
//0000000100006150 adrp x9, #0x100009000 ; 0x1000095d8@PAGE
//0000000100006154 add x9, x9, #0x5d8 ; 0x1000095d8@PAGEOFF, _g
//00000001000095d8 在 MachOView 中 為 0xC 也就是 12,定義全局變量 global_g = 12
//0000000100006158 ldr w10, [x9] ; _g
int w10 = global_g;
//000000010000615c add w8, w8, w10
w8 += w10;
//0000000100006160 ldr w10, [sp, #0x10 + var_8]
w10 = b;
//0000000100006164 add w8, w8, w10
w8 += w10;
//0000000100006168 str w8, [sp, #0x10 + var_C]
//000000010000616c ldr w8, [sp, #0x10 + var_C]
//0000000100006170 mov x0, x8
//0000000100006174 ldp x29, x30, [sp, #0x10]
//0000000100006178 add sp, sp, #0x20
//000000010000617c ret
// ; endp
return w8;
}
精簡后:
int global_g = 12;
int func2(int a, int b) {
printf("test");
return a + global_g + b;
}
??:從上往下還原,代碼執(zhí)行流程不關(guān)心,只關(guān)心結(jié)果和原始的一樣。
if識別
int g = 12;
void func(int a, int b) {
if (a > b) {
g = a;
} else {
g = b;
}
}

0000000100006190 sub sp, sp, #0x10 ; CODE XREF=_main+32
0000000100006194 str w0, [sp, #0xc]
0000000100006198 str w1, [sp, #0x8]
000000010000619c ldr w8, [sp, #0xc]
00000001000061a0 ldr w9, [sp, #0x8]
a和b比較大小,cmp不影響 w8 和 w9 的值。做減法只影響標記寄存器的值。
00000001000061a4 cmp w8, w9
<= 跳轉(zhuǎn) loc_1000061c0,大于直接往下執(zhí)行不跳轉(zhuǎn)
00000001000061a8 b.le loc_1000061c0
//代碼塊1
00000001000061ac ldr w8, [sp, #0xc]
00000001000061b0 adrp x9, #0x100009000
00000001000061b4 add x9, x9, #0x5d0 ; _g
00000001000061b8 str w8, x9
這里b跳轉(zhuǎn)loc_1000061d0,跳過了else的代碼
00000001000061bc b loc_1000061d0
//代碼塊2
loc_1000061c0:
00000001000061c0 ldr w8, [sp, #0x8] ; CODE XREF=_func+24
00000001000061c4 adrp x9, #0x100009000
00000001000061c8 add x9, x9, #0x5d0 ; _g
00000001000061cc str w8, x9
loc_1000061d0:
00000001000061d0 add sp, sp, #0x10 ; CODE XREF=_func+44
00000001000061d4 ret
還原代碼
int global = 12;
void func2(int a, int b) {
// 0000000100006190 sub sp, sp, #0x10 ; CODE XREF=_main+32
//兩個參數(shù)
// 0000000100006194 str w0, [sp, #0xc]
// 0000000100006198 str w1, [sp, #0x8]
// 000000010000619c ldr w8, [sp, #0xc]
// 00000001000061a0 ldr w9, [sp, #0x8]
int w8 = a, w9 = b;
// a和b比較大小,cmp不影響 w8 和 w9 的值。做減法只影響標記寄存器的值。
// 00000001000061a4 cmp w8, w9
// <= 跳轉(zhuǎn) loc_1000061c0,大于直接往下執(zhí)行不跳轉(zhuǎn)
// 00000001000061a8 b.le loc_1000061c0
if (w8 > w9) {
// //代碼塊1
// 00000001000061ac ldr w8, [sp, #0xc]
w8 = a;
// 00000001000061b0 adrp x9, #0x100009000
// 00000001000061b4 add x9, x9, #0x5d0 ; _g
//MachOView中查到 00000001000095d0 為 0xC 4字節(jié)
// 00000001000061b8 str w8, x9
global = w8;
// 這里b跳轉(zhuǎn)loc_1000061d0,跳過了else的代碼
// 00000001000061bc b loc_1000061d0
} else {
// //代碼塊2
// loc_1000061c0:
// 00000001000061c0 ldr w8, [sp, #0x8] ; CODE XREF=_func+24
w8 = b;
// 00000001000061c4 adrp x9, #0x100009000
// 00000001000061c8 add x9, x9, #0x5d0 ; _g
// 00000001000061cc str w8, x9
global = w8;
}
// loc_1000061d0:
// 00000001000061d0 add sp, sp, #0x10 ; CODE XREF=_func+44
//ret前沒有x8和x0的操作,返回void
// 00000001000061d4 ret
}
精簡后:
int global = 12;
void func2(int a, int b) {
if (a > b) {
global = a;
} else {
global = b;
}
}
cmp(Compare)比較指令
cmp 把一個寄存器的內(nèi)容和另一個寄存器的內(nèi)容或立即數(shù)進行比較。但不存儲結(jié)果,只是正確的更改標志(cpsr)。
一般cmp做完判斷后會進行跳轉(zhuǎn),后面通常會跟上b指令!
b 跳轉(zhuǎn)指令
b本身代表跳轉(zhuǎn),后面跟標號會有其他操作:
-
bl 標號:跳轉(zhuǎn)到標號處執(zhí)行,并且影響lr寄存器的值。用于函數(shù)返回。 -
br 寄存器:根據(jù)寄存器中的值跳轉(zhuǎn)。 -
b.gt 標號:比較結(jié)果是 大于(greater than) 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.ge 標號:比較結(jié)果是 大于等于(greater than or equal to) 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.lt 標號:比較結(jié)果是 小于(less than) 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.le 標號:比較結(jié)果是 小于等于(less than or equal to) 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.eq 標號標號:比較結(jié)果是 等于(equal) 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.ne 標號標號:比較結(jié)果是 不等于(not equal) 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.hi 標號標號:比較結(jié)果是 無符號大于 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.hs 標號:比較結(jié)果是 無符號大于等于 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.lo 標號:比較結(jié)果是 無符號小于 執(zhí)行標號,否則不跳轉(zhuǎn)。 -
b.ls 標號:比較結(jié)果是 無符號小于等于 執(zhí)行標號,否則不跳轉(zhuǎn)。
??:cmp后跟的標號條件是else。
循環(huán)
do-while
void func() {
int nSum = 0;
int i = 0;
do {
nSum = nSum + 1;
i++;
} while (i < 100);
}

while
void func() {
int nSum = 0;
int i = 0;
while (i < 100) {
nSum = nSum + 1;
i++;
}
}

for
void func() {
int nSum = 0;
for (int i = 0; i < 100; i++) {
nSum = nSum + 1;
}
}

??
for和while的匯編代碼相同。
Switch 選擇指令
void func(int a) {
switch (a) {
case 1:
printf("case 1");
break;
case 2:
printf("case 2");
break;
case 3:
printf("case 3");
break;
default:
printf("case default");
break;
}
}

在case < 3的情況下底層匯編就是if-else。
修改代碼讓case多于3個:
void func(int a) {
switch (a) {
case 1:
printf("case 1");
break;
case 2:
printf("case 2");
break;
case 3:
printf("case 3");
break;
case 4:
printf("case 4");
break;
default:
printf("case default");
break;
}
}
int main(int argc, char * argv[]) {
func(2);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
分析匯編代碼:
TestDemo`func:
0x102c220b4 <+0>: sub sp, sp, #0x20 ; =0x20
0x102c220b8 <+4>: stp x29, x30, [sp, #0x10]
0x102c220bc <+8>: add x29, sp, #0x10 ; =0x10
//參數(shù)入棧 2
0x102c220c0 <+12>: stur w0, [x29, #-0x4]
//參數(shù)存入w8
-> 0x102c220c4 <+16>: ldur w8, [x29, #-0x4]
//參數(shù)和最小case相減 這里是減1 給到 w8(這里相減有可能溢出)
0x102c220c8 <+20>: subs w8, w8, #0x1 ; =0x1
//x8的值存入x9
0x102c220cc <+24>: mov x9, x8
//從0~31取出來給x9,目的是把高32位清零。相當(dāng)于拿到低32位數(shù)據(jù)。
0x102c220d0 <+28>: ubfx x9, x9, #0, #32
//比較 x9 和 0x3 的值。這里 0x3是 最大 case 和 最小 case 的差值。相當(dāng)于 (參數(shù) - 最小case - (最大case - 最小case))
0x102c220d4 <+32>: cmp x9, #0x3 ; =0x3
//x8低32位入棧
0x102c220d8 <+36>: str x9, [sp]
//比較結(jié)果無符號大于 跳轉(zhuǎn)default。相當(dāng)于不在差值區(qū)間也就是匹配不了case的情況下直接走default。
0x102c220dc <+40>: b.hi 0x102c22138 ; <+132> at main.m
//在區(qū)間中的情況下
//x8的值為 0x102c22150,對應(yīng)的內(nèi)存中值為 fffffffa8 -88
0x102c220e0 <+44>: adrp x8, 0
0x102c220e4 <+48>: add x8, x8, #0x150 ; =0x150
//這時候從棧中取數(shù)據(jù)給x11,棧中目前是x9。x9為(參數(shù) - 最小case = 1)
0x102c220e8 <+52>: ldr x11, [sp]
//x8 + (x11 << 2)的值給到 x10。[x8內(nèi)存地址 + 4] = 移到下一個位置(0x102c22154) 這里對應(yīng)的值為-72。??ret的地址為0x102c2214c + 4 = 0x102c22150,0x102c22150 + 4 = 0x102c22154
//這里計算完x10 = -72
0x102c220ec <+56>: ldrsw x10, [x8, x11, lsl #2]
//x8 + 偏移找到的負數(shù) 給到 x9 0x102c22150 - 72(0x48) = 0x102C22108
0x102c220f0 <+60>: add x9, x8, x10
//跳轉(zhuǎn)寄存器的地址 0x102C22108 對應(yīng) case 2 的地址
0x102c220f4 <+64>: br x9
//case中語句
0x102c220f8 <+68>: adrp x0, 1
0x102c220fc <+72>: add x0, x0, #0xf5d ; =0xf5d
0x102c22100 <+76>: bl 0x102c22588 ; symbol stub for: printf
0x102c22104 <+80>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22108 <+84>: adrp x0, 1
0x102c2210c <+88>: add x0, x0, #0xf64 ; =0xf64
0x102c22110 <+92>: bl 0x102c22588 ; symbol stub for: printf
0x102c22114 <+96>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22118 <+100>: adrp x0, 1
0x102c2211c <+104>: add x0, x0, #0xf6b ; =0xf6b
0x102c22120 <+108>: bl 0x102c22588 ; symbol stub for: printf
0x102c22124 <+112>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22128 <+116>: adrp x0, 1
0x102c2212c <+120>: add x0, x0, #0xf72 ; =0xf72
0x102c22130 <+124>: bl 0x102c22588 ; symbol stub for: printf
0x102c22134 <+128>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22138 <+132>: adrp x0, 1
0x102c2213c <+136>: add x0, x0, #0xf79 ; =0xf79
0x102c22140 <+140>: bl 0x102c22588 ; symbol stub for: printf
0x102c22144 <+144>: ldp x29, x30, [sp, #0x10]
0x102c22148 <+148>: add sp, sp, #0x20 ; =0x20
0x102c2214c <+152>: ret
0x102c220e0 <+44>: adrp x8, 0
0x102c220e4 <+48>: add x8, x8, #0x150 ; =0x150
以上語句執(zhí)行后x8的值:

明顯能夠看出來是一個負數(shù)
0xfffffffa8 ,在指向的地址中有一連串的負數(shù)。
(lldb) p *(int *)0x102c22150
(int) $5 = -88
(lldb) p (int)0xffffffa8
(int) $6 = -88
(lldb)

核心邏輯:
-
subs w8, w8, #0x1:參數(shù)-最小case -
ubfx x9, x9, #0, #32:從0~31取出來給x9,目的是把高32位清零。相當(dāng)于拿到低32位數(shù)據(jù)。 -
cmp x9, #0x3:0x3為最大case - 最小case -
b.hi 0x102c22138: 大于為了判斷(參數(shù) - 最小case)是否在(最大case - 最小case)區(qū)間。無符號為了處理負數(shù),否則負數(shù)永遠小于,用無符號就永遠大于直接排除掉了。 -
0x102c220e0 <+44>: adrp x8, 0
0x102c220e4 <+48>: add x8, x8, #0x150 ; =0x150:
找到跳轉(zhuǎn)表(jump table)地址,這張?zhí)D(zhuǎn)表在編譯的時候生成是連續(xù)的,永遠在程序結(jié)束的后面。 -
ldrsw x10, [x8, x11, lsl #2]:[跳轉(zhuǎn)表地址 +(參數(shù)-最小case)<< 2] 得到跳轉(zhuǎn)表中的偏移值。<<2是因為表中數(shù)據(jù)是4個字節(jié)。這里為差值 * 4 -
add x9, x8, x10:上面的到的偏移值加上跳轉(zhuǎn)表地址就得到了case執(zhí)行的地址。
計算過程
-
參數(shù) - 最小case得到表中index。 -
index與 (最大case - 最小case)無符號比較判斷是否在區(qū)間內(nèi)。 - 不在區(qū)間內(nèi)直接跳轉(zhuǎn)
defalult。 - 在區(qū)間內(nèi)
表頭地址 + index << 2獲取偏移地址(為負數(shù))。 - 跳轉(zhuǎn)
表頭地址 + 偏移地址執(zhí)行對應(yīng)case邏輯。
??:表中為什么不直接存地址? 1.地址過長 2.有ASLR的存在
switch語句的分支比較少的時候(< 3的時候沒有意義)沒有必要使用表結(jié)構(gòu),相當(dāng)于if。各個分支常量的差值較大的時候,編譯器會在效率還是內(nèi)存進行取舍,這個時候編譯器還是會編譯成類似于
if-else的結(jié)構(gòu)。
比如:100、200、300、400這種case還是和if-else相同,10、20、30、40會生成一張表。所以在寫switch邏輯的時候最好使用連續(xù)的值。至于具體邏輯編譯器會根據(jù)case和差值進行優(yōu)化選擇。case越多,差值越小,數(shù)值越連貫 編譯器會生成跳轉(zhuǎn)表,否則還是if-else。在分支比較多的時候:在編譯的時候會生成一個表(跳轉(zhuǎn)表每個地址四個字節(jié))。
跳轉(zhuǎn)表中數(shù)量為
最大case - 最小case + 1為一共有多少種可能性。switch分支的代碼是連續(xù)的。由于case是連貫的用空間換時間。
總結(jié)
