[C in ASM(ARM64)]第六章 結構體

6.1 結構體基礎

結構體在運行時的本質是一片按照結構體定義分配的內存。比如如下結構體:

struct point {n int x;n int y;n};n

其本質就是分配兩個int大小的連續內存,內存的前一個int大小的內容是x,後一個int大小的內容是y。

來一段簡單的例子:

#include <stdio.h>nnstruct point {n int x;n int y;n};nnvoid main()n{n struct point p;n p.x = 1;n p.y = 2;n}n

其彙編數字如下:

_main: ; @mainn; BB#0:ntsubtsp, sp, #16 ; =16ntorrtw8, wzr, #0x2ntorrtw9, wzr, #0x1ntstrtw9, [sp, #8]ntstrtw8, [sp, #12]ntaddtsp, sp, #16 ; =16ntretn

其實現是把數字1(值在w8寄存器)放入棧頂下偏移量8的位置,把數字2(值在w9寄存器)放入棧頂下偏移量4的位置。

那更複雜一點的結構體中的結構體呢?

struct point {n int x;n int y;n};nnstruct rect {n struct point pt1;n struct point pt2;n}n

其本質就是分配連續2個struct point的空間也就是4個int。(這裡我們暫時不考慮對齊等問題)

#include <stdio.h>nstruct point {n int x;n int y;n};nnstruct rect {n struct point pt1;n struct point pt2;n};nnvoid main()n{n struct rect r;n r.pt1.x = 1;n r.pt1.y = 2;n r.pt2.x = 3;n r.pt2.y = 4;n}n

其彙編如下:

sub sp, sp, #16 ; =16n orr w8, wzr, #0x4n orr w9, wzr, #0x3n orr w10, wzr, #0x2n orr w11, wzr, #0x1n str w11, [sp]n str w10, [sp, #4]n str w9, [sp, #8]n str w8, [sp, #12]n add sp, sp, #16 ; =16n retn

首先先把對應的數值存到寄存器里(w8-w11)。結構的成員變數按照偏移從依此從低地址分布到高地址,加上ARM64的地址空間符合遞減的特徵,即棧頂所在內存地址高於棧底地址(簡單理解為SP地址)。所以,1存入[sp],2存入[sp, #4],依此類推。

6.2 結構體與函數

先看看結構體作為函數返回值的例子:

在ARM64下,不同大小的結構體返回值處理各不相同,請各位仔細閱讀本節。

先來看看結構體大小小於等於8byte的時候返回值是如何處理的:

#include <stdio.h>nstruct point {n int x;n int y;n};nnstruct point makepoint(int x, int y)n{n struct point p;n p.x = x;n p.y = y;n return p;n}nnvoid main()n{n struct point retP = makepoint(4, 5);n}n

其彙編實現如下:

_makepoint: ; @makepointn; BB#0:ntsubtsp, sp, #32 ; =32ntstrtw0, [sp, #20]ntstrtw1, [sp, #16]ntldrtw0, [sp, #20]ntstrtw0, [sp, #8]ntldrtw0, [sp, #16]ntstrtw0, [sp, #12]ntldrtw0, [sp, #8]ntstrtw0, [sp, #24]ntldrtw0, [sp, #12]ntstrtw0, [sp, #28]ntldrtx0, [sp, #24]ntaddtsp, sp, #32 ; =32ntretnn_main: ; @mainn; BB#0:ntsubtsp, sp, #32 ; =32ntstptx29, x30, [sp, #16] ; 8-byte Folded Spillntaddtx29, sp, #16 ; =16ntorrtw0, wzr, #0x4ntmovtw1, #5ntblt_makepointntstrtx0, [sp, #8]ntldptx29, x30, [sp, #16] ; 8-byte Folded Reloadntaddtsp, sp, #32 ; =32ntretn

首先從main函數看起,基於ARM64 Calling Convention,先將參數0和1放入w0w1寄存器里,跳轉到符號_makepoint執行函數。

然後再讓我們來看看makepoint函數,大家千萬別被這裡的LDR和STR指令嚇到,我們來一點點剖析。

strtw0, [sp, #20]nstrtw1, [sp, #16]n

上述這兩句,毫無疑問是將我們的參數0和1保存到棧上。然後我們只要一直追蹤這兩個數據源即可(中間一大堆指令在優化開啟的情況是可以省略的)。

因此,我們最終看到,1的值存入到了[sp, #24],而2的值存入到了[sp, #28]。

ldrtx0, [sp, #24]n

還記得我們的返回值是放入x0寄存器的嗎?我們把上述結構體的值存入x0寄存器作為返回值即可。(要注意LD/ST系列指令針對w/x不同長度的寄存器取值長度不同的特點)

那我們在看看如果是struct rect呢?x0就放不下了:

#include <stdio.h>nstruct point {n int x;n int y;n};nnstruct rect {n struct point pt1;n struct point pt2;n};nnstruct rect makerect(int x1, int y1, int x2, int y2)n{n struct rect r;n r.pt1.x = x1;n r.pt1.y = y1;n r.pt2.x = x2;n r.pt2.y = y2;n return r;n}ntnvoid main()n{n struct rect retR = makerect(1, 2, 3, 4);n}n

其彙編實現如下:

_makerect: ; @makerectn; BB#0:n sub sp, sp, #48 ; =48n str w0, [sp, #28]n str w1, [sp, #24]n str w2, [sp, #20]n str w3, [sp, #16]n ldr w0, [sp, #28]n str w0, [sp]n ldr w0, [sp, #24]n str w0, [sp, #4]n ldr w0, [sp, #20]n str w0, [sp, #8]n ldr w0, [sp, #16]n str w0, [sp, #12]n ldr q0, [sp]n str q0, [sp, #32]n ldr x0, [sp, #32]n ldr x1, [sp, #40]n add sp, sp, #48 ; =48n retnn_main: ; @mainn; BB#0:n sub sp, sp, #32 ; =32n stp x29, x30, [sp, #16] ; 8-byte Folded Spilln add x29, sp, #16 ; =16n orr w0, wzr, #0x1n orr w1, wzr, #0x2n orr w2, wzr, #0x3n orr w3, wzr, #0x4n bl _makerectn str x1, [sp, #8]n str x0, [sp]n ldp x29, x30, [sp, #16] ; 8-byte Folded Reloadn add sp, sp, #32 ; =32n retn

struct rect這個結構體佔據了4個int,即16byte。我們直接看最核心的部分:

ldr x0, [sp, #32]nldr x1, [sp, #40]n

看到沒,在這種情況下。結構體被分為兩個8 byte的部分分別放入x0x1寄存器來進行返回。並且main函數中的

str x1, [sp, #8]nstr x0, [sp]n

也充分證明了這點。

那如果數據是浮點數或者數據量太大寄存器不夠用怎麼辦?放棧上!同時把棧上的地址放入x8寄存器進行返回。具體的實現請參見[ARM64下Indirect Result Location摸索](ARM64下Indirect Result Location摸索)

說完返回值再來說說參數,參數也很類似:

#include <stdio.h>nstruct point {n int x;n int y;n};nnstruct point addpoint(struct point p1, struct point p2)n{n p1.x += p2.x;n p1.y += p2.y;n return p1;n}n

其彙編實現如下:

_addpoint: ; @addpointn; BB#0:ntsubtsp, sp, #32 ; =32ntstrtx0, [sp, #16]ntstrtx1, [sp, #8]ntldrtw8, [sp, #8]ntldrtw9, [sp, #16]ntaddttw8, w9, w8ntstrtw8, [sp, #16]ntldrtw8, [sp, #12]ntldrtw9, [sp, #20]ntaddttw8, w9, w8ntstrtw8, [sp, #20]ntldrtw8, [sp, #16]ntstrtw8, [sp, #24]ntldrtw8, [sp, #20]ntstrtw8, [sp, #28]ntldrtx0, [sp, #24]ntaddtsp, sp, #32 ; =32ntretn

兩個point參數被放入了x0x1兩個64位寄存器中,然後取出加了一把,然後返回值放入x0進行返回。

6.3聯合體

聯合體是個很神奇的東西,同一片內存裡面根據取的方式取出來的內容就不一樣,十分具有彙編風格。數據沒有類型,決定數據類型的是數據被怎麼用!來個例子:

#include <stdio.h>nunion u_tag {n int ival;n float fval;n char *sval;n};ntnvoid main() {n union u_tag u;n u.ival = 1;n u.fval = 1.0;n u.sval = "hello, world";n int a = u.ival;n float b = u.fval;n char *c = u.sval;n}n

其彙編實現如下:

_main: ; @mainn; BB#0:ntsubtsp, sp, #32 ; =32ntadrptx8, l_.str@PAGEntaddtx8, x8, l_.str@PAGEOFFntfmovts0, #1.00000000ntorrtw9, wzr, #0x1ntstrtw9, [sp, #24]ntstrts0, [sp, #24]ntstrtx8, [sp, #24]ntldrtw9, [sp, #24]ntstrtw9, [sp, #20]ntldrts0, [sp, #24]ntstrts0, [sp, #16]ntldrtx8, [sp, #24]ntstrtx8, [sp, #8]ntaddtsp, sp, #32 ; =32ntretnn.sectiont__TEXT,__cstring,cstring_literalsnl_.str: ; @.strnt.ascizt"hello, world"n

從這三行,我們不難發現,我們分別用不同的數據類型(從高級編程語言角度)往[sp, #24]裡面進行數據改寫。其中x8是字元串的地址,s0是浮點數,w9是整數。當然,後面放的會把前面的覆蓋掉! 取操作也是一樣的,這裡不再贅述,留給讀者自行閱讀。

還有一個細節,一般情況下union的大小是根據最大的那個成員的數據類型來的(需要考慮對齊,也可以人為修改),在放其他較小的成員時空間會略有浪費。

6.4 bit-field

bit-field是一個巨牛的東西,它可以極度優化內存使用,不過時間換空間嘛,存取速度也會稍微慢些。來個例子:

#include <stdio.h>nvoid main() {n struct {n unsigned int is_keyword:1;nt unsigned int is_extern:1;nt unsigned int is_static:1;n } flags;n flags.is_keyword = 1;n flags.is_extern = 0;n flags.is_static = 1;n}n

其彙編實現如下:

_main: ; @mainn; BB#0:ntsubtsp, sp, #16 ; =16ntmovtw8, #251ntmovtw9, #253ntldrbtw10, [sp, #8]ntandtw10, w10, #0xfentorrtw10, w10, #0x1ntandtw10, w10, #0xffntstrbtw10, [sp, #8]ntldrbtw10, [sp, #8]ntandttw9, w10, w9ntandtw9, w9, #0xffntstrbtw9, [sp, #8]ntldrbtw9, [sp, #8]ntandttw8, w9, w8ntandtw8, w8, #0xffntorrtw8, w8, #0x4ntandtw8, w8, #0xffntstrbtw8, [sp, #8]ntaddtsp, sp, #16 ; =16ntretn

這裡需要注意一點,ARM64下,結構體大小默認按8 byte對齊。因此sizeof(flags)一般是8。

什麼是對齊呢?讀者可以自行對比如下兩個struct:

struct A {n char c;n char b;n int k;n}nnstruct B {n char c;n int k;n char b;n}n

對齊要考慮兩個因素,成員變數的地址對齊以及結構體自身空間對齊。

言歸正傳,我們這邊利用bit-field的技術,將每一個成員變數都使用1 bit進行存儲。因此,對於寄存器來說,它始終只要操作1 byte的數據長度即可,因此彙編中出現了大量的ldrb指令,

and w10, w10, #0xfenorr w10, w10, #0x1nand w10, w10, #0xffn

我們可以看看幹了些啥。0xfe即二進位1111 1110,進行and操作,保留w10寄存器最後1 byte的值(當然最後一位也清零0)。然後或操作,將最後一位置為1,存入[sp, #8]中。

我們繼續往下看指令:

and w9, w10, w9nand w9, w9, #0xffn

w9的初始值為253,即二進位1111 1101。通過and操作,將第二位置為0,存入[sp, #8]

and w8, w8, #0xffnorr w8, w8, #0x4nand w8, w8, #0xffn

上述三條就是將倒數第三位置為1,原理不再贅述。

通過bit-field可以大量節省空間,但是存數據的時候卻用了N多條額外的計算指令來取得byte中特定的位,計算效率相對傳統的movLD/ST搭配,多了不少指令,自然也會慢一點。


推薦閱讀:

iOS 9.3.5緊急發布背後真相:NSO使用iPhone 0day無需點擊遠程攻破蘋果手機(8月26日 12:10更新)
一個 8 年 iOS 開發者的五個建議
iOS 的日曆 icon 能換成純黑色? | 一日一技
提升 iOS 開發效率! Xcode 9 內置模擬器的9個技巧
談談 MVX 中的 Model

TAG:ARM | iOS | 汇编语言 |