匯編語言知多少(四): AT&T 匯編語法

在前幾篇文章里我們一直聊的是 Intel 格式的 8086匯編, 這篇文章我們聊聊 AT&T 格式的匯編語法.

AT&T VS Intel

  1. 基于 x86 架構(gòu) 的處理器所使用的匯編指令一般有兩種格式.
  • Intel 匯編
    • DOS(8086處理器), Windows
    • Windows 派系 -> VC 編譯器
  • AT&T匯編
    • Linux, Unix, Mac OS, iOS(模擬器)
    • Unix派系 -> GCC編譯器
  1. 基于ARM 架構(gòu) 的處理器所使用的匯編指令一般有一種格式, 這種處理器常用語嵌入式設(shè)備, 移動(dòng)設(shè)備, 以高性能, 低能耗見長(zhǎng)
  • ARM 匯編, iOS 真機(jī).

64位 AT&T匯編的寄存器

  1. 有16個(gè)常用的64位寄存器
  • %rax, %rbx, %rcx , %rdx, %rsi, %rdi, %rbp, %rsp (和 8086匯編類似 )
  • %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15
  1. 寄存器的具體用途
  • %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
    1. 我們的源代碼經(jīng)過編譯器編譯成匯編指令, 從左到右依次為
      指令在內(nèi)存中的地址 <+(和上一個(gè)指令的偏移地址差)> 匯編指令 源操作數(shù) 目標(biāo)操作數(shù) ; 注釋
    1. 匯編分析, 關(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
    1. 復(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)行程序.

大致流程如下:

    1. 判斷 __CFConstantStringClassReferencePtr + 7 是不是 等于 0 , 如果是,則函數(shù)執(zhí)行完畢, 否則, 繼續(xù)向下執(zhí)行.(這個(gè)類索引值常量 我查到的結(jié)果是 與編譯器內(nèi)置的decl 匹配)
    1. object_getClass, class_getName 獲取當(dāng)前對(duì)象的類名
    1. 通過調(diào)用函數(shù) asprintf , 按照 _NSZombie_%s 格式化, 并存儲(chǔ)到寄存器 rdi 中.
    1. 通過調(diào)用函數(shù) objc_lookUpClass蛙婴,查找新類名的類是否存在,不存在,則創(chuàng)建.
    1. 通過調(diào)用函數(shù) objc_lookUpClass筝闹,獲取名為 _NSZombie_ 的類, 這個(gè)類 是系統(tǒng)類.
    1. 通過調(diào)用函數(shù) objc_duplicateClass, 復(fù)制 _NSZombie_ 類媳叨,生成新的 _NSZombie_%s 類, 并將原來的 _NSZombie_ 類釋放掉.
    1. 通過調(diào)用函數(shù) object_setClass,將當(dāng)前對(duì)象的類型設(shè)置成新的 _NSZombie_%s 類,
    1. 判斷 __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 捕獲到.

維基百科-匯編語言

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市议双,隨后出現(xiàn)的幾起案子痘番,更是在濱河造成了極大的恐慌,老刑警劉巖平痰,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汞舱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宗雇,警方通過查閱死者的電腦和手機(jī)昂芜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赔蒲,“玉大人泌神,你說我怎么就攤上這事∥枋” “怎么了欢际?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)矾兜。 經(jīng)常有香客問我损趋,道長(zhǎng),這世上最難降的妖魔是什么焕刮? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任舶沿,我火速辦了婚禮,結(jié)果婚禮上配并,老公的妹妹穿的比我還像新娘括荡。我一直安慰自己,他們只是感情好溉旋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布畸冲。 她就那樣靜靜地躺著,像睡著了一般观腊。 火紅的嫁衣襯著肌膚如雪邑闲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天梧油,我揣著相機(jī)與錄音苫耸,去河邊找鬼。 笑死儡陨,一個(gè)胖子當(dāng)著我的面吹牛褪子,可吹牛的內(nèi)容都是我干的量淌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嫌褪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼呀枢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起笼痛,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤裙秋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后缨伊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摘刑,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年刻坊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泣侮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡紧唱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出隶校,到底是詐尸還是另有隱情漏益,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布深胳,位于F島的核電站绰疤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏舞终。R本人自食惡果不足惜轻庆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望敛劝。 院中可真熱鬧余爆,春花似錦、人聲如沸夸盟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽上陕。三九已至桩砰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間释簿,已是汗流浹背亚隅。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庶溶,地道東北人煮纵。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓懂鸵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親醉途。 傳聞我的和親對(duì)象是個(gè)殘疾皇子矾瑰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容