[C in ASM(ARM64)]第五章 指標和陣列
C程式設計語言的彙編解釋 第五章 指標和陣列
在前面的文章中提到過, 在彙編層面, 所有的資料都存在棧/符號/暫存器中, 棧和符號都屬於記憶體範疇, 進一步抽象就是, 所有的資料都在記憶體和暫存器中(這裡先不討論協處理器或者外部儲存等其他範疇)。 而指標的本質就是記憶體的地址!
5。1 指標和地址
指標通常被認為是c語言中比容易繞暈的一個特性, 而實際上指標的本質十分簡單, 比如如下程式碼:
#include
void empty()
{
int a = 1;
void *b = &a;
}
其彙編形式如下:
。section __TEXT,__text,regular,pure_instructions
。ios_version_min 11, 2
。globl _empty
。p2align 2
_empty: ; @empty
; BB#0:
sub sp, sp, #16 ; =16
add x8, sp, #12 ; =12
orr w9, wzr, #0x1
str w9, [sp, #12]
str x8, [sp]
add sp, sp, #16 ; =16
ret
。subsections_via_symbols
從之前的介紹中知道變數`a`實際上是存放在棧上的, 在這裡是`sp + 12`也就是sp往上12byte的位置, 取地址操作`&a`實際上取到的就是`sp + 12`這個值(也就是`add x8, sp, #12`), 然後將地址從x8放入sp中(`str x8, [sp]`),這裡的`sp`就是變數`b`在棧上的地址。
也就是說, 如果一個變數b裡存放的是另一個變數a的地址, 那麼就可以稱為, b是指向a的指標了!
用匯編的說法來解釋, 就是一個地址指向的記憶體裡面存的內容是另一個地址!
在彙編層面有一個很重要的點就是要對思想做轉變, 在彙編層面沒有變數/物件和指標等概念, 只有暫存器/記憶體和資料, 而資料是沒有型別的(資料只是一個位元序列)! 資料該怎麼理解要看具體的彙編指令是怎麼操作暫存器和記憶體的, 如果使用整數加法指令操作暫存器, 那麼資料將被當做整數, 同理, 如果使用浮點數加法指令操作暫存器那麼資料將被當做浮點數! 如果指令對暫存器做定址操作, 那麼資料將被當做地址(也就值指標)!
5。2 指標和函式引數
在第四章中已經介紹了函式的引數是透過暫存器和棧來傳遞的, 如果我們傳遞的引數是一個整數, 那麼暫存器/棧中存的就是整數, 那如果我們傳的引數是一個指標, 那麼暫存器/棧中存的就是地址! 比如如下程式碼:
#include
void swap(int *px, int *py) {
int temp;
temp = *px;
*px = *py;
*py = temp;
}
int main()
{
int a = 1;
int b = 2;
swap(&a, &b);
return 0;
}
其彙編實現如下(節選):
#呼叫部分
add x0, sp, #8 ; =8
add x1, sp, #4 ; =4
orr w8, wzr, #0x2
orr w9, wzr, #0x1
stur wzr, [x29, #-4]
str w9, [sp, #8]
str w8, [sp, #4]
bl _swap
#實現部分
str x0, [sp, #24]
str x1, [sp, #16]
ldr x0, [sp, #24]
ldr w8, [x0]
str w8, [sp, #12]
ldr x0, [sp, #16]
ldr w8, [x0]
ldr x0, [sp, #24]
str w8, [x0]
ldr w8, [sp, #12]
ldr x0, [sp, #16]
str w8, [x0]
其關鍵流程就是:
在呼叫部分中(main),前兩句`add`指令取變數`a`和`b`的地址放入x0和x1中作為呼叫_swap的引數,後面orr指令和str指令是向地址中存值,這裡的stur這句是無用程式碼可以理解為為了對齊。
在實現部分中(swap),前面兩句str把x0和x1作為px和py引數放入他們在棧上分配的空間裡。然後把x0再撈出來,再取x0作為地址裡存的值放入w8。w8放入`sp + 12`也就是給temp分配的空間。下面就是取px作為地址裡的值,放入py做地址的記憶體空間裡。最後從temp中撈出之前存的py做地址裡存的值放入px做地址的記憶體空間裡。
5。3 指標和陣列
陣列是棧上提前分配好的一片記憶體, 如果你拿到了這片記憶體的地址(指標), 那麼你將可以隨意訪問它。
在日常的使用中, 可能經常會碰見如下兩種訪問陣列的方法, 如`a[1]`和`*(a + 1)`, 它們有什麼區別呢? 看程式碼:
#include
void f1() {
int a[2];
int b = a[1];
}
void f2() {
int a[2];
int b = *(a+1);
}
其彙編實現:
_f1: ; @f1
; BB#0:
sub sp, sp, #48 ; =48
stp x29, x30, [sp, #32] ; 8-byte Folded Spill
add x29, sp, #32 ; =32
adrp x8, ___stack_chk_guard@GOTPAGE
ldr x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
ldr x8, [x8]
adrp x9, ___stack_chk_guard@GOTPAGE
ldr x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
ldr x9, [x9]
stur x9, [x29, #-8]
ldr w10, [sp, #20]
str w10, [sp, #12]
adrp x9, ___stack_chk_guard@GOTPAGE
ldr x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
ldr x9, [x9]
ldur x11, [x29, #-8]
cmp x9, x11
str x8, [sp] ; 8-byte Folded Spill
b。ne LBB0_2
; BB#1:
ldp x29, x30, [sp, #32] ; 8-byte Folded Reload
add sp, sp, #48 ; =48
ret
LBB0_2:
bl ___stack_chk_fail
。globl _f2
。p2align 2
_f2: ; @f2
; BB#0:
sub sp, sp, #48 ; =48
stp x29, x30, [sp, #32] ; 8-byte Folded Spill
add x29, sp, #32 ; =32
adrp x8, ___stack_chk_guard@GOTPAGE
ldr x8, [x8, ___stack_chk_guard@GOTPAGEOFF]
ldr x8, [x8]
adrp x9, ___stack_chk_guard@GOTPAGE
ldr x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
ldr x9, [x9]
stur x9, [x29, #-8]
ldr w10, [sp, #20]
str w10, [sp, #12]
adrp x9, ___stack_chk_guard@GOTPAGE
ldr x9, [x9, ___stack_chk_guard@GOTPAGEOFF]
ldr x9, [x9]
ldur x11, [x29, #-8]
cmp x9, x11
str x8, [sp] ; 8-byte Folded Spill
b。ne LBB1_2
; BB#1:
ldp x29, x30, [sp, #32] ; 8-byte Folded Reload
add sp, sp, #48 ; =48
ret
LBB1_2:
bl ___stack_chk_fail
完全一樣有沒有! 其中的關鍵點在於:
從這裡可以推斷出陣列頂部邊界用到的`___stack_chk_guard`的地址是`stur x9, [x29, #-8]`(也就是`sp + 24`),也就意味著陣列的內容是放在`sp + 20`和`sp + 16`裡,既然陣列只有兩個元素,那`sp + 16`這個低地址就是陣列`a`的頭部指標啦。
ldr w10, [sp, #20]
str w10, [sp, #12]
這兩句是`int b = a[1];`的實現, `sp + 20`是陣列頭指標`sp+16`加上偏移量`4`的產物, 而這裡`4`是`sizeof(int)`。
5。4 指標的算術運算
指標(地址)和整數很像, 可以做加減乘除等算術運算, 只不過在做運算的時候, 加減的增量的`1`是代表著一個元素的大小。 比如如下程式碼:
#include
int main()
{
int *a;
a++;
return 0;
}
其彙編實現(關鍵部分):
ldr x9, [sp]
add x9, x9, #4 ; =4
即`a++`的實現, 加的數字是`4`而不是`1`, 因為`int`的大小是`4`。
再比如如下程式碼:
#include
int main()
{
char *a;
a++;
return 0;
}
其彙編實現(關鍵部分):
ldr x9, [sp]
add x9, x9, #1 ; =1
即`a++`的實現, 加的數字是`1`, 因為`char`的大小是`1`。