最近看一些資料,接觸到一些資料有匯編陷揪,發(fā)現(xiàn)看不懂惋鸥,還是有必要學(xué)習(xí)下。
在iOS中悍缠,消息發(fā)送是匯編寫的揩慕,在學(xué)習(xí)戴銘高手課時hook oc中的objc_send方法,也用到匯編扮休。嘗試去閱讀,網(wǎng)上也有很多注釋拴鸵,發(fā)現(xiàn)看完之后依然一知半解玷坠。還是從基礎(chǔ)學(xué)起蜗搔。
1. 先看一段C的代碼,我們可以放到main.m中:
int addFunction(int a, int b) {
int c = a + b;
return c;
}
2. 選擇真機八堡,Xcode選中Assemble轉(zhuǎn)成匯編代碼
這樣我們可以看到匯編樟凄,注意一定要選擇Generic iOS Device,模擬器或真機轉(zhuǎn)成的匯編和本文有差異兄渺。
3. _addFunction:
忽略掉.file缝龄、.loc、.cfi_startproc挂谍,可得到以下相關(guān)匯編代碼:
sub sp, sp, #16 ; =16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16 ; =16
ret
為什么是w0,w1呢叔壤,不是r0,r1或x0,x1呢?
w0 - w30訪問時口叙,訪問的是這些寄存器的低32位炼绘。當(dāng)使用 r0 - r30 訪問時,它就是一個64位的數(shù)妄田。
3.1 添加注釋后的匯編
首先俺亮,分配棧所需的所有臨時存儲空間。棧是一大塊函數(shù)隨時想使用的內(nèi)存疟呐。
ARM中的棧內(nèi)存是高地址向低地址分布的脚曾,意味著你必須從棧指針開始減。
在這里启具,分配了16個字節(jié)長度的內(nèi)存本讥。
sub sp, sp, #16 ; =16
這里,兩個參數(shù)被存入棧中富纸。這是通過存儲寄存指令(str)實現(xiàn)的囤踩。
第一個參數(shù)是要存儲的寄存器
第二個是存儲的位置。方括號代表里面值是內(nèi)存地址晓褪。 這個方括號指令允許你為一個值指定偏移量堵漱,
因此[sp, #12]的意思『在棧指針的地址上加上12字節(jié)偏移量』
同樣地,str w0, [sp, #12]意味著『存儲w0寄存器的值到棧指針地址加上12字節(jié)內(nèi)存的位置』涣仿。
str w0, [sp, #12]
str w1, [sp, #8]
剛被保存到棧的值又被讀取到相同的寄存器內(nèi)勤庐。和str指令相反的,ldr指令是從一個內(nèi)存中加載內(nèi)容到寄存器好港。
ldr w0, [sp, #12]意思是『讀取出在棧指針地址加上12字節(jié)內(nèi)存的位置的內(nèi)容愉镰,并將內(nèi)容賦值給寄存器w0』。
如果你好奇為何w0和w1剛被存儲又被加載出來钧汹,對丈探,它是很奇怪,這兩行明明就是多余的嘛拔莱!如果編譯器允許基本的編譯優(yōu)化碗降,那么這多余的就會被消除隘竭。
ldr w0, [sp, #12]
ldr w1, [sp, #8]
意思是將w0和w1中的內(nèi)容相加,并將相加的值賦值給r0讼渊。
add指令入?yún)⒖梢允莾蓚€或者三個动看,如果是三個,那第一個參數(shù)就是存儲后兩個參數(shù)相加的值的寄存器爪幻。
所以菱皆,這行指令也可以寫成:add w0, w0, w1。
add w0, w0, w1
再一次挨稿,編譯器生成了一些多余的代碼:將相加的結(jié)果存儲起來仇轻,又讀取到相同的位置。
str w0, [sp, #4]
ldr w0, [sp, #4]
函數(shù)即將終止叶组,因此棧指針放回原來的地方拯田。
函數(shù)開始時從sp(棧指針)上減去了12個字節(jié)而得到12個字節(jié)內(nèi)存使用。現(xiàn)在它把12個字節(jié)還回去甩十。
函數(shù)必須保證棧指針操作平衡船庇,否則棧指針可能漂移,最終可能超出了已分配的內(nèi)存侣监。你應(yīng)該不希望那樣...
add sp, sp, #16 ; =16
ret
3.2 簡單點理解
sub sp, sp, #16 ; =16 //棧地址減去16鸭轮,即分配了16字節(jié)內(nèi)存
str w0, [sp, #12] //把w0存儲到sp棧中,sp指針上加上12字節(jié)的偏移量
str w1, [sp, #8] //把w1存儲到sp棧中橄霉,sp指針上加上8字節(jié)的偏移量
ldr w0, [sp, #12] //讀取棧中12字節(jié)偏移量的地址到存儲器w0中
ldr w1, [sp, #8] //讀取棧中8字節(jié)偏移量的地址到存儲器w1中
add w0, w0, w1 //w0的值加上w1的值存儲到w0中
str w0, [sp, #4]//把w0的值存儲到sp棧中
ldr w0, [sp, #4]//讀取sp棧中的值到w0中
add sp, sp, #16 ; =16 //棧地址加16字節(jié)窃爷,即回收分配的內(nèi)存
ret //結(jié)束
以上是未優(yōu)化的匯編代碼,有很多重復(fù)且沒有用的代碼。
3.3 Xcode選擇release姓蜂,編譯器優(yōu)化后:
add w0, w1, w0
ret
小結(jié):
- iOS中對象內(nèi)存是分配在堆上的按厘,局部變量或指針都是在棧上的。OC代碼都會被機器編譯成匯編钱慢,不考慮復(fù)雜的場景逮京,匯編都是和棧的內(nèi)存進(jìn)行打交道。匯編也和CPU的寄存器打交道束莫。
- 局部變量的值或地址在棧中懒棉,而真正的計算是在寄存器中的,使用時需要分配空間览绿,即sub sp, sp, #16 ;寄存器可以把值存儲到棧中str w0, [sp, #12];寄存器也可以從棧中取出值策严,即 ldr w1, [sp, #8];寄存器和寄存器之間也可以相互操作;用完需要釋放內(nèi)存,不然會有內(nèi)存泄露饿敲,即add sp, sp, #1妻导。
參考文章:iOS匯編教程
4. arm64寄存器簡單介紹
64位處理器有34個寄存器,包括31個通用寄存器、SP倔韭、PC暑脆、CPSR。
x0-x7: 用于子程序調(diào)用時的參數(shù)傳遞狐肢,x0 還用于返回值傳遞
x0 - x30 是31個通用整形寄存器。每個寄存器可以存取一個64位大小的數(shù)沥曹。 當(dāng)使用 r0 - r30 訪問時份名,它就是一個64位的數(shù)。當(dāng)使用 w0 - w30 訪問時妓美,訪問的是這些寄存器的低32位
查看x0返回值,0x2e=46
pc:表示當(dāng)前執(zhí)行的指令的地址
(lldb) register read pc
pc = 0x00000001022c6c50 DataStructureDemo`addFunction + 28 at main.m:14:12
lr:鏈接寄存器僵腺,存放著函數(shù)的返回地址:這里存放的是fooFunction地址
(lldb) register read lr
lr = 0x00000001022c6c74 DataStructureDemo`fooFunction + 24 at main.m:18:9
5. _fooFunction:
在上面的代碼中添加下面函數(shù),并調(diào)用addFunction
void fooFunction() {
int add = addFunction(12, 34);
printf("add = %i", add);
}
5.1 添加注釋后的匯編
push {r7, lr} //r7,lr入棧
mov r7, sp//r7=sp即r7保存了棧頂元素
sub sp, #8//sp減8字節(jié)
movs r0, #12 //r0 = 12
movs r1, #34//r1 = 34
bl _addFunction //調(diào)用函數(shù)addFunction; r0,r1是addFunction兩個參數(shù)
str r0, [sp, #4]//r0是返回結(jié)果,把r0存儲到sp中
ldr r1, [sp, #4]//取出sp給r1;這兩句等價于r1=r0;
movw r0, :lower16:(L_.str-(LPC2_0+4))
movt r0, :upper16:(L_.str-(LPC2_0+4))
//printf函數(shù)的第一個參數(shù)是一個字符串,可以搜索L_.str壶栋,看到.asciz "add = %i"
//前兩個指令加載常量的地址辰如,并減去標(biāo)簽的地址(LPC1_0加上4字節(jié))。
add r0, pc
//r0加上pc(程序計數(shù)器)贵试,這樣無論L.str在二進(jìn)制文件的什么位置都能夠準(zhǔn)確的存放字符串的位置琉兜。
//上面三條指令加載指向所需的字符串的開始地址的指針到r0寄存器。
bl _printf//執(zhí)行printf函數(shù),r0是參數(shù)毙玻,且字符串已拼接好
str r0, [sp] @ 4-byte Spill//存儲r0到棧中
add sp, #8//恢復(fù)棧內(nèi)存
pop {r7, pc}//恢復(fù)r7,pc
6.OC函數(shù)
前面的寫法都是C語言的寫法豌蟋,OC與C還是有一定區(qū)別∩L玻看下面源碼:
- (int)addValue:(int)a toValue:(int)b {
int c = a + b;
return c;
}
6.1優(yōu)化版本匯編
adds r0, r3, r2
bx lr
r3,r2是參數(shù)a和b,為什么不是r0,r1呢?
因為在OC中有兩個隱士的參數(shù):id self, SEL _cmd梧疲。
6.2 foo函數(shù)
- (void)foo {
int add = [self addValue:12 toValue:34];
NSLog(@"add = %i", add);
}
轉(zhuǎn)換后的匯編:
push {r7, lr}//r7,lr入棧
mov r7, sp//r7=sp
Ltmp10:
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))
Ltmp11:
movs r2, #12//r2=12
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))//查找_cmd
movs r3, #34//r3=34
LPC4_0:
add r1, pc//r1=r1+pc
ldr r1, [r1]//表示加載存儲在r1指針內(nèi)的內(nèi)容并賦值給r1。用偽代碼表示r1=*r1
bl _objc_msgSend//調(diào)用objc_msgsend
Ltmp12:
mov r1, r0//r1=ro
Ltmp13:
movw r0, :lower16:(L__unnamed_cfstring_-(LPC4_1+4))
movt r0, :upper16:(L__unnamed_cfstring_-(LPC4_1+4))
//給r0賦值,r0=self;
LPC4_1:
add r0, pc//r0=r0+pc
bl _NSLog//調(diào)用NSLog
Ltmp14:
pop {r7, pc}//出棧
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_.2-(LPC4_0+4))//查找_cmd
add r1, pc//r1=r1+pc
這三句放一塊解讀运准,沒什么問題幌氮,r1存入當(dāng)前selector的字符。selector的引用:其實selector就是存儲在數(shù)據(jù)段的字符串胁澳。movw r0, :lower16:(L__unnamed_cfstring_-(LPC4_1+4))
movt r0, :upper16:(L__unnamed_cfstring_-(LPC4_1+4))
add r0, pc//r0=r0+pc
這三句與r1的三句類似该互,r0=self。
3.總的上面步驟:r0=self,r1=_cmd,r2=12,r3=34;調(diào)用objc_msgSend,調(diào)用NSLog听哭。整體流程清晰明了慢洋。
總結(jié):
1.至此大概了解了OC的整個匯編的過程.舉一反三,看下viewDidLoad方法:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
轉(zhuǎn)成匯編:
push {r7, lr}
mov r7, sp
//只要是方法里調(diào)用了別的方法,上面兩句少不了陆盘。
sub sp, #8//分配1個字節(jié)
movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
Ltmp1:
movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_0+4))
//r1=cmd
movw r2, :lower16:(L_OBJC_CLASSLIST_SUP_REFS_$_-(LPC0_1+4))
movt r2, :upper16:(L_OBJC_CLASSLIST_SUP_REFS_$_-(LPC0_1+4))
//r2=super
add r1, pc//r1+=pc
add r2, pc//r2+=pc
ldr r1, [r1]//r1=*r1;表示加載存儲在r1指針內(nèi)的內(nèi)容并賦值給r1
ldr r2, [r2]//r2=*r2;表示加載存儲在r2指針內(nèi)的內(nèi)容并賦值給r2
strd r0, r2, [sp]//str r0, [sp];str r2, [sp + 4]即r0普筹,r2存儲到sp中
mov r0, sp//r0=sp
bl _objc_msgSendSuper2//調(diào)用super方法
add sp, #8//恢復(fù)棧指針
pop {r7, pc}//恢復(fù)r7,pc
如果是匯編,能不能反推到正常的代碼邏輯呢隘马,找到bl即找到了調(diào)用的方法太防,再找到r0,r1,r2等參數(shù),知道方法的參數(shù)。也許復(fù)雜的邏輯很難蜒车,但孰能生巧讳嘱。