從 簡單彙編基礎 到 Swift 不簡單的 a + 1

看完這篇彙編你就知道我有多會編程序員

做爲iOS開發,程序崩潰猶如屢見不鮮,秉着沒有崩潰也要製造崩潰的原則編程

我天天都吃的很飽swift

但學藝不精的我常常有這樣的困擾,每次崩潰都定位到一堆 相似緩存

movq $0x0, 0xc7a(%rip) 的天書裏面,慌亂的我 只能狂點下一步sass

逃離這些洪水猛獸bash

是誰悄然無聲打開了 Always Show Disassembly 這扇大門?函數

但三過家門而又不入,也並不是個人性格ui

著名的學者 沃滋基說過:克服困難最好的解決方式 ,就是不克服spa

因而 我把門關上了命令行

初識彙編

垃圾桶彙編法 顧名思義 就是 我不懂彙編,但我也要和垃圾桶同樣會裝

雖然我不知道movq是什麼意思,但我知道move

move 的 意思是 移動

至於q,管它 q不q 的,可是e 沒了

忽然想到個人亞索沒有了e ,那我還怎麼快樂的move

我很生氣,決定深刻了解一下這個東西

彙編語言

彙編語言:(assembly language) 是一種用於 電子計算機、微處理器、微控制器,或其餘可編程器件的低級語言 - 維基百科

簡單來講,咱們平時寫的代碼都是 高級語言,計算機不理解高級語言,就像你吃飯不吃塑料包裝同樣,你吃的是裏面的東西

彙編語言是二進制指令的 文本形式,計算機會把 咱們的代碼 轉換爲 彙編語言,彙編語言 經過機器指令 還原成 二進制代碼,也就是所謂的 0 1,計算機就能夠執行了。

每個 CPU 的 機器指令不一樣,因此對應的彙編語言也不一樣。  

寄存器

爲何須要了解寄存器?

由於彙編語言 的數據存儲 與寄存器和內存 息息相關

通常來講,數據是放在內存中的,CPU 計算的時候就去內存裏拿數據,可是

CPU 的運算速度 > 內存的運算速度

就彷彿

你吃飯的速度 > 食堂大媽打菜的速度

你受不了,大媽受得了嗎?

因此CPU 自帶了一級,二級緩存,至關於大媽讓她兒子給你送飯

問題是這個中間層仍是慢且不穩定

CPU 緩存的數據地址是 不固定的,意味着你點了份 西紅柿蓋澆飯,讓店員給你送到座位上,店員找了半個小時,發現你坐在別人店裏

...

食屎吧雷.jpeg

因此CPU 有了寄存器,來存儲頻繁使用的數據。CPU 經過寄存器 跟 內存 間接交換數據

寄存器都有本身的名稱(如 rax ,rdx等)

你說你坐在C區21號,店員還不是分分鐘把飯塞到你嘴裏,質問你:喂,你還要飯嗎?

因此CPU 會去 指定名稱的 寄存器拿數據,這樣速度就不快了嘛

天下武功,惟快不破。

因此爲何須要寄存器,由於它的讀寫速度夠快

內存

說到底,寄存器依舊是一個暫存區,只是一箇中間站,真正存儲數據,操做數據的仍是內存

如下是內存分佈圖:

這裏簡單介紹一下堆棧

  • heap

    • 分配方式:alloc,速度相對棧比較慢,容易產生內存碎片
    • 管理方式: 程序員,ARC下面,堆區的分配和釋放基本也是系統操做
    • 地址分佈:從低到高,非連續
    • 大小:取決於計算機系統的有效的虛擬空間
    • 做用:動態分配內存,存儲變量,延長生命週期
  • stack

    • 一端進行插入和刪除操做的特殊線性表
    • 分配方式: 系統,速度比較快
    • 管理方式: 系統,不受程序員控制
    • 地址分佈:從高到低,連續
    • 大小:棧頂的地址和容量是系統決定
    • 生命週期:出了做用域就會釋放
    • 入棧出棧:先進後出,相似羽毛球筒,先放入的羽毛球,老是最後才能拿到

在Linux 下,iterm2 敲下ulimit -a,能夠看到棧分配的默認大小爲 8192 ,也就是 8M

-t: cpu time (seconds)              unlimited
-f: file size (blocks)              unlimited
-d: data seg size (kbytes)          unlimited
-s: stack size (kbytes)             8192
複製代碼

彙編語言

由於是iOS開發,因此就只稍微瞭解了 AT&T 彙編 的皮毛

雖然看起來會枯燥一點,可是理解這些比較經常使用的寄存器,對彙編代碼的理解就會有質的飛躍

以前你是門外漢

如今好歹算個半個彙編人

今天的你比昨天更博學了

iOS 模擬器、MAC OS、Linux : AT&T彙編 ; 

iOS 真機: ARM 彙編
複製代碼

x86-64 中,AT&T 中經常使用的 寄存器有 16種:

  • %rax、%rbx、%rcx、%rdx、%rsi、%rdi、%rbp、%rsp
  • %r八、%r九、%r十、%r十一、%r十二、%r1三、%r1四、%r15

經常使用寄存器

AT&T 經常使用寄存器介紹:

%rax:常做爲函數返回值。 通常來講,爲了向後兼容,64位的寄存器會兼容32的寄存器,32和64能夠一塊兒使用

64位: 8個字節 ,以 r 開頭; 32位: 4個字節,以e 開頭,看圖

在64位的寄存器 rax中,爲了兼容分配了較低的32位,也就是4個字節 給了 eax。基本上,彙編出現的eax 就是 表明rax,eax是 rax 的一部分,其餘 部分寄存器同理

%rdi、%rsi、%rdx、%rcx、%r八、%r9: 常做爲函數參數

r8,r9 這種32位的表示法,一般在後面加d,如r8d,r9d

%rip: 指令指針,存儲CPU 即將執行的指令地址

  • 解釋一下rip
即將執行: 下一條執行
指令地址: 開頭的那一串 0x100...

截取2句彙編:

 7 --  0x100000a64 <+20>:  movq   $0x1, 0x719(%rip)
 8 --  0x100000a6f <+31>:  movl   %edi, -0x34(%rbp)
複製代碼

第7行中的 0x719(%rip) 中的 rip 就是指令指針,即將執行的 地址 就是 第8行 開頭的那個地址0x100000a6f

因此這裏rip 的地址就是 0x100000a6f,有了rip 的地址

通常來講

0x719(%rip) 就是 0x719 + %rip地址

-0x719(rip) 就是 %rip - 0x719
複製代碼

棧相關

%rbp: 棧基址指針也稱爲幀指向,指向棧底

%rsp: 棧指針,指向棧頂

經常使用指令

一些比較常見的我能理解的指令

中文 AT&T 翻譯
當即數 $0x1 當即數就是常量,前面加$表示
尋址 mov movq $0x1, %rdi 將 1 賦值給 寄存器 rdi,從左往右
內存賦值 lea leaq %rbp,%rax 將rbp的 內存地址值 賦給 rax
異或 xor xorl %eax, %eax 將eax 清0,本身異或本身
跳轉 jmp jmp 0x80001 跳轉到函數地址爲0x80001的地址
間接跳轉 *() jmp *(%rax) rax是個內存地址,*(rax) 是拿到rax地址裏的值
函數調用 call callq 0x80001 調用 0x80001的地址的函數,通常配合retq

那麼這個q 是幹什麼的呢 ?callq ,leaq ,movq 都有q?

這裏的q 是 表明字節大小

b:byte 字節,操做位寬 1個字節
w:word ,2個字節
l:long ,4個字節
q: 8個字節

q意味着,寄存器操做的數據類型 須要佔用  8個字節,固然這根據你的數據大小決定
複製代碼

因此上面那句代碼  

movq $0x1, 0x719(%rip)

意思是,當即數 1 尋址 (0x719 + %rip),並賦值。將 1 賦值給 (0x719 + 0x100000a6f) 這個地址,當即數1 佔用了 8個字節

讀取寄存器

介紹幾個 lldb 的經常使用指令,能夠方便咱們查閱 寄存器的值

  • register read/格式: 讀取寄存器的值
register read/x rax   // 讀取寄存器 rax 裏面的值

x:16進制
f:浮點
d:10進制
複製代碼

  • register write 修改寄存器的值
(lldb) register read/x rax
     rax = 0x0000000000000003
(lldb) register write rax 4 // 修改成4
(lldb) register read/x rax
     rax = 0x0000000000000004
複製代碼

  • x/數量-格式-字節大小: 讀取內存中的值
x/4xg 0x1000002   

// 將 0x1000002 地址的值,以8個字節的格式,分紅4份,16進制 展現


// 這裏是展現 和 上面的操做不太同樣,g 表示8個字節
b - byte 1字節
h - half word 2字節
w - word 4字節
g - giant word 8字節

🐷:若是數據的值不夠分紅4份,剩下的字節以0 補齊
複製代碼

棧幀

幀,在電影中指每一張畫面,一種平均單位

棧幀:站着的幀,畫面立體了起來,不僅僅是一個角度,裏面包含了不少信息

包含了

每一次* 函數調用涉及的相關信息

局部變量、函數返回地址、函數參數等
複製代碼

咱們都知道,函數的調用是會在棧上分配內存的,分配多少取決於函數的參數和局部變量

那麼一個函數的佔用的內存大小,函數的返回地址,咱們就須要保存起來,這就用到了棧幀

  • 爲何須要保存函數的信息?

由於函數運行完畢 ,在棧上須要釋放內存,以及繼續執行上一層代碼,咱們須要上一層函數的返回地址,在本次函數執行完畢後,恢復父函數的棧幀結構

想象這樣一個場景

類比一下接力賽中,4位選手

棧頂 1 -> 2 -> 3 -> 4 棧底,每一位選手都要在拿到接力棒後,纔會開跑

那麼 1號選手,就須要保存2號選手的信息,他不須要知道 3號 和 4號

下一個接棒者 長什麼樣?身上的號碼牌?站在哪裏?

1 號選手結束以後, 賽場隊伍就只剩  2 -> 3 -> 4,此時焦點就集中在2號選手

選手跑步    -> 函數調用
選手信息    -> 棧幀保存的信息
視線焦點    -> 棧指針,指向當前選手

只有咱們清楚了下一位的接棒人(在棧中對應上一層函數)

咱們才能在本次結束以後找到正確的位置,繼續執行流程
複製代碼

至於信息的保存者? 取決於寄存器的標識 Caller Save 和 Callee Save

  當子函數調用的時候,也會用到父函數的寄存器,可能會存在覆蓋寄存器的值。

* Caller Save,調用者保存

父函數調用子函數以前,將寄存器的值保存一份,這樣子函數就能夠隨意覆蓋


* Callee Save,被調用者保存

父函數不保存,交由子函數 保存和恢復 寄存器的值
複製代碼

例子

咱們簡單的創建一個 命令行 工程,打開彙編 Always Show Disassembly

用 Swift 寫出如下代碼

func test() -> Int {
    var a = 3
    a = a + 1
    return a
}

-> test()  // 斷點指向test,run
複製代碼

程序運行起來,咱們能夠看到 ,程序斷點在 test 函數調用的地方

zzz`main:
    0x100000bc0 <+0>:  pushq  %rbp
    0x100000bc1 <+1>:  movq   %rsp, %rbp
    0x100000bc4 <+4>:  subq   $0x20, %rsp
    0x100000bc8 <+8>:  movl   %edi, -0x4(%rbp)
    0x100000bcb <+11>: movq   %rsi, -0x10(%rbp)
->  0x100000bcf <+15>: callq  0x100000bf0               ; zzz.test() -> Swift.Int at main.swift:189
    0x100000bd4 <+20>: xorl   %edi, %edi
    0x100000bd6 <+22>: movq   %rax, -0x18(%rbp)
    0x100000bda <+26>: movl   %edi, %eax
    0x100000bdc <+28>: addq   $0x20, %rsp
    0x100000be0 <+32>: popq   %rbp
    0x100000be1 <+33>: retq   
複製代碼

咱們控制檯 用 si 進入 test 函數內部

能夠看到 test 內部的彙編代碼,參考下面的圖,說一說個人理解

zzz`test():
->  0x100000bf0 <+0>:  pushq  %rbp
    0x100000bf1 <+1>:  movq   %rsp, %rbp
    0x100000bf4 <+4>:  movq   $0x0, -0x8(%rbp)
    0x100000bfc <+12>: movq   $0x3, -0x8(%rbp)
    0x100000c04 <+20>: movq   $0x4, -0x8(%rbp)
    0x100000c0c <+28>: movl   $0x4, %eax
    0x100000c11 <+33>: popq   %rbp
    0x100000c12 <+34>: retq   
複製代碼

  • 借圖,侵刪

子函數調用時,調用者與被調用者的棧幀結構

分析

test 函數 一進來,就執行了下面兩句代碼

->  0x100000bf0 <+0>:  pushq  %rbp
    0x100000bf1 <+1>:  movq   %rsp, %rbp
複製代碼

一開始,test 函數 就進行了 壓棧

pushq %rbp

壓棧的是父函數 main函數棧幀指針 %rbp

% rbp指向的返回地址, 是main 函數 調用完 test ,應該回到哪裏的地址,也就是當前函數test 調用開始時 棧的位置

而此時 test 函數的 %rbp ,至關因而新的%rbp

  而後經過

movq %rsp, %rbp

將%rsp 也 指向 %rbp,test 棧幀 的初始位置

由於%rsp 老是指向新的元素,因此在被 一些局部變量等 填充以後,來到了棧頂

函數的調用: 棧幀被建立 -> 填充 -> 銷燬

接着

0x100000bf4 <+4>:  movq   $0x0, -0x8(%rbp)
    0x100000bfc <+12>: movq   $0x3, -0x8(%rbp)
    0x100000c04 <+20>: movq   $0x4, -0x8(%rbp)
複製代碼

當即數 0 ,賦值給 %rbp - 0x8的 8個字節 的內存空間 用於初始化

後面又將 參數3,覆蓋,以及計算+1 的值 繼續覆蓋,這裏應該是省略了 +1 的操做

接着

movl $0x4, %eax

前面說過,rax 一般做爲返回值,eax 是 rax 的32位表示,將 當即數4賦值給 eax做爲返回值

這裏用到了movl 和 eax,是由於 int類型佔用4個字節,只須要 4個字節便可,而寄存器 是8個字節,因此寄存器的操做後綴 是q

到如今 咱們就獲得了 test函數的 返回值 4

再來

0x100000c11 <+33>: popq   %rbp
    0x100000c12 <+34>: retq   
複製代碼

前有 push ,後就有pop,將test 中的寄存器 %rbp 從棧中彈出,恢復調用前的 rbp,而

retq 等價於 popq %rip,前面說過rip 表明着 下一條指令

將%rip 指令指針,重新指回 test 函數調用後的 下一條 指令,這樣程序就能夠繼續運行了

此時的 內存分佈

而 test 函數的內存空間,隨着做用域的結束,就被釋放了

到底爲止,咱們看到了 簡單的 test 函數 a + 1的 小小的過程

請勿見笑

結語

雖然是簡單的一個加法,可是倒是咱們入門的好盆友

相信看完此篇,此時的你一定熱血沸騰,心潮澎湃

忍不住

想把門關上

...

由於個人理解也不夠深,認知有限,若是有錯誤的理解,還請指正。

謝謝~ 撒花

參考

函數調用棧

x86-64 下函數調用及棧幀原理

相關文章
相關標籤/搜索