在前幾篇文章里我們一直聊的是 Intel 格式的 8086匯編, 這篇文章我們聊聊 AT&T 格式的匯編語法.
AT&T VS Intel
- 基于 x86 架構(gòu) 的處理器所使用的匯編指令一般有兩種格式.
-
Intel 匯編
- DOS(8086處理器), Windows
- Windows 派系 -> VC 編譯器
-
AT&T匯編
- Linux, Unix, Mac OS, iOS(模擬器)
- Unix派系 -> GCC編譯器
- 基于ARM 架構(gòu) 的處理器所使用的匯編指令一般有一種格式, 這種處理器常用語嵌入式設(shè)備, 移動(dòng)設(shè)備, 以高性能, 低能耗見長(zhǎng)
- ARM 匯編, iOS 真機(jī).
64位 AT&T匯編的寄存器
- 有16個(gè)常用的64位寄存器
- %rax, %rbx, %rcx , %rdx, %rsi, %rdi, %rbp, %rsp (和 8086匯編類似 )
- %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15
- 寄存器的具體用途
- %rax 作為函數(shù)返回值使用.
- %rsp 指向棧頂.
- %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10等寄存器用于存放函數(shù)參數(shù).
64位, 32位, 16位, 8位 寄存器的顯示.
棧幀
這兩張圖雖然高地址的方向是反的, 但他們說的是同一個(gè)問題
- 函數(shù)的調(diào)用流程(內(nèi)存)
- 1.push 參數(shù)
- 2.push 函數(shù)的返回地址
- 3.push bp (保留bp之前的值,方便以后恢復(fù))
- 4.mov bp, sp (保留sp之前的值凤瘦,方便以后恢復(fù))
- 5.sub sp,空間大小 (分配空間給局部變量)
- 6.保護(hù)可能要用到的寄存器
- 7.使用CC(int 3)填充局部變量的空間
- 8.--------執(zhí)行業(yè)務(wù)邏輯--------
- 9.恢復(fù)寄存器之前的值
- 10.mov sp, bp (恢復(fù)sp之前的值)
- 11.pop bp (恢復(fù)bp之前的值)
- 12.ret (將函數(shù)的返回地址出棧逆趣,執(zhí)行下一條指令)
- 13.恢復(fù)棧平衡 (add sp,參數(shù)所占的空間)
調(diào)試
在解析匯編程序的時(shí)候, 有一些 LLDB 指令是很好用的
- 讀取寄存器的值: register read/x $rax, 這里x 指 16進(jìn)制格式, 還有 f 浮點(diǎn)數(shù), d 十進(jìn)制數(shù)
- 修改寄存器的值: register write $rax 0
- 讀取內(nèi)存中的值:
- x/數(shù)量-格式-字節(jié)大小 內(nèi)存地址
- x/3xw 0x0000010, 這里 w 指的是4個(gè)字節(jié)大小
- b, byte, 1字節(jié); h, hard word, 2字節(jié); w, word, 4字節(jié); g, giant word, 8字節(jié).
- 修改內(nèi)存中的值:
- memory write 內(nèi)存地址 數(shù)值
- memory write 0x0000010 10
- 尋址: image lookup --address 內(nèi)存地址
還有 JCC 的指令表
指令 | 解釋 | 描述 |
---|---|---|
JE, JZ | equal, zero | 結(jié)果為零則跳轉(zhuǎn)(相等時(shí)跳轉(zhuǎn)) |
JNE, JNZ | not equal, not zero | 結(jié)果不為零則跳轉(zhuǎn)(不相等時(shí)跳轉(zhuǎn)) |
JS | sign(有符號(hào)\有負(fù)號(hào)) | 結(jié)果為負(fù)則跳轉(zhuǎn) |
JNS | not sign(無符號(hào)\無負(fù)號(hào)) | 結(jié)果為非負(fù)則跳轉(zhuǎn) |
JP, JPE | parity even | 結(jié)果中1的個(gè)數(shù)為偶數(shù)則跳轉(zhuǎn) |
JNP, JPO | parity odd | 結(jié)果中1的個(gè)數(shù)為偶數(shù)則跳轉(zhuǎn) |
JO | overflow | 結(jié)果溢出了則跳轉(zhuǎn) |
JNO | not overflow | 結(jié)果沒有溢出則跳轉(zhuǎn) |
JB, JNAE | below, not above equal | 小于則跳轉(zhuǎn) (無符號(hào)數(shù)) |
JNB, JAE | not below, above equal | 大于等于則跳轉(zhuǎn) (無符號(hào)數(shù)) |
JBE, JNA | below equal, not above | 小于等于則跳轉(zhuǎn) (無符號(hào)數(shù)) |
JNBE, JA | not below equal, above | 大于則跳轉(zhuǎn)(無符號(hào)數(shù)) |
JL, JNGE | little, not great equal | 小于則跳轉(zhuǎn) (有符號(hào)數(shù)) |
JNL, JGE | not little, great equal | 大于等于則跳轉(zhuǎn) (有符號(hào)數(shù)) |
JLE, JNG | little equal, not great | 小于等于則跳轉(zhuǎn) (有符號(hào)數(shù)) |
JNLE, JG | not little equal, great | 大于則跳轉(zhuǎn)(有符號(hào)數(shù)) |
實(shí)戰(zhàn)1: 計(jì)算 (a++) + (a++) + (a++) = ?
這次我們選擇創(chuàng)建一個(gè)簡(jiǎn)單的 Swift 項(xiàng)目, 運(yùn)行在iOS模擬器中. 代碼如下, 由于 Swift 已經(jīng)不支持 a++, ++a 這種操作, 所以我自定義實(shí)現(xiàn)了一個(gè).
在 Xcode 的菜單欄中, Debug -> Debug workflow -> 選擇 Always Show Disassembly, 這是控制是否顯示匯編程序
在項(xiàng)目中設(shè)置斷點(diǎn), 程序運(yùn)行到斷點(diǎn)處, 觸發(fā)中斷, Xcode 界面顯示當(dāng)前程序的匯編界面.
接下來我們來解讀一下這些匯編指令
0x10d9b6c96 <+118>: movq $0x1, -0x28(%rbp)
0x10d9b6c9e <+126>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++ postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
- 我們的源代碼經(jīng)過編譯器編譯成匯編指令, 從左到右依次為
指令在內(nèi)存中的地址 <+(和上一個(gè)指令的偏移地址差)> 匯編指令 源操作數(shù) 目標(biāo)操作數(shù) ; 注釋
- 我們的源代碼經(jīng)過編譯器編譯成匯編指令, 從左到右依次為
- 匯編分析, 關(guān)鍵代碼都有注釋
0x10d9b6c79 <+89>: movq 0x45f8(%rip), %rsi ; "viewDidLoad"
0x10d9b6c80 <+96>: movq %rdx, -0x50(%rbp)
0x10d9b6c84 <+100>: callq 0x10d9b8354 ; symbol stub for: objc_msgSendSuper2
0x10d9b6c89 <+105>: movq -0x48(%rbp), %rdi
0x10d9b6c8d <+109>: callq 0x10d9b835a ; symbol stub for: objc_release
調(diào)用完 super.viewDidLoad()
0x10d9b6c92 <+114>: leaq -0x28(%rbp), %rdi
0x10d9b6c96 <+118>: movq $0x1, -0x28(%rbp)
<注釋>上面可以翻譯成 mov $0x1 [rbp-0x28] 將立即數(shù)1 賦值到 [rbp-0x28] 所指的內(nèi)存單元
<注釋>這是一個(gè) 局部變量, 對(duì)應(yīng)源代碼中的 int a = 1.
0x10d9b6c9e <+126>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++ postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
<注釋> 調(diào)用 ++ 函數(shù)
0x10d9b6ca3 <+131>: leaq -0x28(%rbp), %rdi
0x10d9b6ca7 <+135>: movq %rax, -0x58(%rbp)
<注釋> 此時(shí) %rax 中的值為 1
0x10d9b6cab <+139>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++
<注釋> 調(diào)用 ++ 函數(shù)
postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
0x10d9b6cb0 <+144>: movq -0x58(%rbp), %rdx
0x10d9b6cb4 <+148>: addq %rax, %rdx
<注釋> %ax 中的值(2) + %rdx 中的值(1) 存儲(chǔ)在 %rdx 寄存器中(3)
0x10d9b6cb7 <+151>: seto %r8b
0x10d9b6cbb <+155>: movq %rdx, -0x60(%rbp)
<注釋>將%rdx中的值賦值給 -0x60(%rbp)
0x10d9b6cbf <+159>: movb %r8b, -0x61(%rbp)
0x10d9b6cc3 <+163>: jo 0x10d9b6daf ; <+399> at ViewController.swift:17
0x10d9b6cc9 <+169>: leaq -0x28(%rbp), %rdi
0x10d9b6ccd <+173>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++ postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
0x10d9b6cd2 <+178>: movq -0x60(%rbp), %rdi
<注釋> %rax 的值為3, %rdi 的值為3
0x10d9b6cd6 <+182>: addq %rax, %rdi
<注釋>3 + 3 = %rdi 的值為 6
0x10d9b6cd9 <+185>: seto %cl
0x10d9b6cdc <+188>: movq %rdi, -0x70(%rbp)
<注釋> 將 %rdi 的值賦給 -0x70(%rbp), 值為6
0x10d9b6ce0 <+192>: movb %cl, -0x71(%rbp)
0x10d9b6ce3 <+195>: jo 0x10d9b6db1 ; <+401> at ViewController.swift:17
0x10d9b6ce9 <+201>: movq -0x70(%rbp), %rax
<注釋>將 -0x70(%rbp) 的值賦給 %rax, 值為6
<注釋>接下來是傳遞參數(shù)打印 c 的值
-> 0x10d9b6ced <+205>: movl $0x1, %ecx
-
- 復(fù)盤整個(gè)過程:
- -0x28(%rbp) 對(duì)應(yīng) 局部變量a, -0x70(%rbp) 對(duì)應(yīng) 局部變量c
- %rax 存放的是每次運(yùn)算的值, 分別為 1, 2, 3,
- %rdi 存放每次相加后的值, 分別為 1, 3, 6. 這里面有一個(gè) %rdx, 存儲(chǔ)過內(nèi)部運(yùn)算的值.
- 最終結(jié)果是 6
下面是一個(gè)挑戰(zhàn)
var a = 2
let c = a++ + a++ + a++ // 2 + 3 + 4 = 9 , a = 5
let c2 = ++a + a++ + a++ // 6 + 6 + 7 = 19, a = 8
let c3 = ++a + ++a + a++ // 9 + 10 + 10 = 29, a = 11
print(c3, a) // 29, 11
實(shí)戰(zhàn)2: 解讀 zombieObject
在 MRC 環(huán)境下, 我們運(yùn)行下面這段代碼.
NSArray *arr = @[@"a", @"b", @"c"];
NSLog(@"1==>%ld", arr.retainCount); // 1
[arr release]; // 0
NSLog(@"1==>%ld", arr.retainCount); // 報(bào)錯(cuò)
[arr release];
NSLog(@"1==>%ld", arr.retainCount);
程序肯定會(huì)報(bào)錯(cuò), EXC_BAD_Address, 這類訪問內(nèi)存錯(cuò)誤的問題, 原因大部分是 向一個(gè)已釋放的對(duì)象發(fā)送消息
如果你對(duì)匯編比較熟悉的話, 直接觀察這個(gè)匯編代碼, 也可以定位問題位置.
但是, 如果你看不懂會(huì)匯編, 一時(shí)找不到錯(cuò)誤, Xcode 已經(jīng)內(nèi)置了工具幫助我們調(diào)試.
在 Edit Scheme —> Diagnostics —> Memory Management —> Zombie Objects
打開 Zombie Objects 后,重新運(yùn)行代碼, 我們會(huì)發(fā)現(xiàn)
- 錯(cuò)誤提示由原來的EXC_BAD_Address 變?yōu)?EXC_BAD_INSTRUCTION
- 控制臺(tái)直接打印出錯(cuò)誤信息, 向一個(gè)已釋放的對(duì)象發(fā)送消息. 這個(gè)原來是沒有的.
- arr 對(duì)象 發(fā)生了改變. 由原來的NSArray -> _NSZombie__NSArrayl
開啟前 | |
---|---|
開啟后 |
- 這新創(chuàng)建的 Zombie__NSArray 是什么呢? 我們可以合理猜測(cè),
- 開啟 Zombie Objects 功能后, 在運(yùn)行程序時(shí), Xcode 內(nèi)部會(huì)檢測(cè)是否向已釋放的對(duì)象發(fā)送消息,
- 如果有, 創(chuàng)建 Zombie Object, 替換它, 并且向這個(gè)新的對(duì)象發(fā)消息, 在控制臺(tái)打印錯(cuò)誤信息.
- 如果不創(chuàng)建新的Object, 原對(duì)象已經(jīng)釋放了, 無法向其發(fā)送消息, 導(dǎo)致無法定位問題.
本著大膽猜想, 小心求證的原則, 接下來我們驗(yàn)證一下.
驗(yàn)證猜想
驗(yàn)證第一步
沒什么不是看源碼不能解決的 :] 如果能找到 Runtime 的源碼就好了.
Apple 是有提供 Runtime 的源碼大致實(shí)現(xiàn). 在這里可以下載到, 它是一個(gè) OC 項(xiàng)目, 下載后打開就可以了.
在搜索框了搜索 zombie, 大致找到了相關(guān)信息, 我整理一下
// Replaced by CF (throws an NSException)
+ (void)dealloc {
}
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
由這我們可以猜想: 對(duì)象在被銷毀的時(shí)候, 程序會(huì)創(chuàng)建 Zombie對(duì)象, 調(diào)用實(shí)例方法
_objc_rootDealloc
,
void
_objc_rootDealloc(id obj)
{
顯示斷言, 顯示被釋放的對(duì)象信息
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
判斷是否該對(duì)象應(yīng)該釋放
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
正式釋放
free(this);
}
else {
繼續(xù)使用
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
在不釋放內(nèi)存的情況下銷毀實(shí)例
刪除關(guān)聯(lián)引用
objc_destructInstance(obj);
正式銷毀
free(obj);
return nil;
}
到這里其實(shí)還是不能看出實(shí)際的東西, 到底是什么時(shí)候被替換的, 替換的過程中做了什么, 在這里沒有體現(xiàn)出來.
驗(yàn)證第二步.
剛才使用的 Runtime 源碼 是.mm文件, 里面除了 OC 和 C 代碼以外還包含C++代碼, 蘋果開源了這一部分的底層代碼.
- 在 CFRuntime.c 中, 同樣是搜索 Zombies, 我們發(fā)現(xiàn)了一個(gè)有趣的函數(shù)
__CFZombifyNSObject(void)
, 翻譯過來就是 zombie 化 Object.
為此, 我們需要添加 符號(hào)斷點(diǎn), 在程序運(yùn)行時(shí), 如果有調(diào)用 __CFZombifyNSObject, 就會(huì)觸發(fā)中斷.
在 Zombie Objects 開啟的情況下, 運(yùn)行程序, 我們會(huì)發(fā)現(xiàn).
NSObejct 替換了 dealloc
和 __dealloc_zombie
這兩個(gè)方法.
我們繼續(xù)設(shè)置符號(hào)斷點(diǎn)為 __dealloc_zombie
. 運(yùn)行程序.
大致流程如下:
- 判斷 __CFConstantStringClassReferencePtr + 7 是不是 等于 0 , 如果是,則函數(shù)執(zhí)行完畢, 否則, 繼續(xù)向下執(zhí)行.(這個(gè)類索引值常量 我查到的結(jié)果是 與編譯器內(nèi)置的decl 匹配)
- object_getClass, class_getName 獲取當(dāng)前對(duì)象的類名
- 通過調(diào)用函數(shù) asprintf , 按照
_NSZombie_%s
格式化, 并存儲(chǔ)到寄存器 rdi 中.
- 通過調(diào)用函數(shù) asprintf , 按照
- 通過調(diào)用函數(shù) objc_lookUpClass蛙婴,查找新類名的類是否存在,不存在,則創(chuàng)建.
- 通過調(diào)用函數(shù) objc_lookUpClass筝闹,獲取名為
_NSZombie_
的類, 這個(gè)類 是系統(tǒng)類.
- 通過調(diào)用函數(shù) objc_lookUpClass筝闹,獲取名為
- 通過調(diào)用函數(shù) objc_duplicateClass, 復(fù)制
_NSZombie_
類媳叨,生成新的_NSZombie_%s
類, 并將原來的_NSZombie_
類釋放掉.
- 通過調(diào)用函數(shù) objc_duplicateClass, 復(fù)制
- 通過調(diào)用函數(shù) object_setClass,將當(dāng)前對(duì)象的類型設(shè)置成新的
_NSZombie_%s
類,
- 通過調(diào)用函數(shù) object_setClass,將當(dāng)前對(duì)象的類型設(shè)置成新的
- 判斷 __CFZombieEnabled 是否為 0 , 若是的, 則釋放掉新的對(duì)象, 否則返回新的對(duì)象.
小結(jié):
- __CFZombifyNSObject(void) 的實(shí)現(xiàn)是這樣的: 程序會(huì)替換掉當(dāng)前對(duì)象 的 dealloc 方法, 實(shí)現(xiàn) __dealloc_zombie 方法, 在方法中創(chuàng)建一個(gè)新的類. 即 Zombie Objecct.
- 當(dāng)對(duì)象的引用計(jì)數(shù)為0時(shí), 會(huì)調(diào)用它的 dealloc方法, 將該對(duì)象轉(zhuǎn)為 zombie object, 當(dāng)向原來已經(jīng)被釋放的對(duì)象發(fā)送消息時(shí), 內(nèi)部會(huì)轉(zhuǎn)到zombie object 代替舊的類接受消息, 由于新的類沒有實(shí)現(xiàn)任何方法关顷,所以程序會(huì)崩潰糊秆,最終被 Xcode 捕獲到.