Programming with 64-Bit ARM Assembly Language: Single Board Computer Development for Raspberry Pi and Mobile Devices

Chapter 6: Functions and the Stack


2021.08.12: updated by

Stacks on Linux

Linux では、プログラムを起動するときにスタックとして8 MByte のメモリを割り当てる。 X31 レジスタは zero register および stack pointer (SP) として利用される。 レジスタの内容をスタックにコピーする命令は STR命令 と STP 命令、 スタックの内容をレジスタにコピーする命令は LDR 命令と LDP 命令である。 ARM では、SP は 16-byte aligned である必要がある。 すなわち、16の倍数だけを加算や減算できる。
    STR  X0, [SP, #-16]!   // SP := SP-16; [SP] := X0
    LDR  X0, [SP], 16      // X0 = [SP]; SP = SP + 16
SP は16-byte align なので、上で #-8 と指定すると Bus Error を起こす。 レジスタは8byte (=64bit)なのに、16byteきざみになるのはもったいないので、 レジスタをペアにしてスタック操作する方がよいだろう。 8byteの転送よりは16byteの転送の方が遅いことには注意が必要である。
    STP  X0, X1, [SP, #-16]!    // SP:=SP-16; [SP] := (X0, X1)
    LDP  X0, X1, [SP], #16      // (X0, X1) := [SP]; SP:=SP+16

Branch with Link

X30 レジスタは link register (LR) として利用される。 Branch with Link (BL) 命令は branch (B)命令と同じだが、 分岐を実行する前に次の命令のアドレスを LR レジスタに入れる点が異なる。 return (RET) 命令を実行すると LRレジスタに保持されているアドレスに分岐する。 命令パイプラインはRET命令が実行されるとLRレジスタのアドレスに分岐することを 知っているので、ペナルティ無しに関数からの戻りを実現できる。
    // ... other code ...
    BL    myfunc
    MOV  X1, #4
    // ... more code ...

myfunc:   // do some work
    RET

Nesting Function Calls 入子になった関数呼び出し

LR の内容をスタックに積んでから、別の BLを実行すればよい。
    // ... other code ...
    BL   myfunc
    MOV  X1, #4
    // ... more code ...
------------------------------
myfunc:
    STR  LR, [SP, #-16]!   // push LR
    // ... do some work ...
    BL   myfunc2
    // ... do some more work ...
    LDR  LR, [SP], #16    // pop LR
    RET
myfunc2:
    // do some work ...
    RET

Function Parameters and Return Values

関数を呼び出す側は、パラメータのうち最初の8個は X0, X1, ..., X7 に入れ、 残りのパラメータはスタックにpushする。 関数からの返り値は、X0に入れる。128-bit整数が必要な場合は X0 と X1 に入れる。 もっと多くのデータを返したい場合は、パラメータのうちの1つをメモリアドレスにしておき、 そこに追加のデータを入れる。

Managing the Registers

Calling routine:
  1. X0 - X18 は必要ならば保存する。
  2. パラメータのうち最初の8個を X0-X7 にコピーする。
  3. 残りのパラメータを スタックにpushする。
  4. BL命令を使って、関数を呼び出す。
  5. X0に入っている返り値を調べる。
  6. X0 - X18 のうち保存していたものを戻す。
Called function:
  1. LR レジスタをスタックにpushする。また、X19-X30のうちこのルーチンで使うものをスタックにpushする。
  2. この関数で必要な動作を行う。
  3. X0に返り値を入れる。
  4. X19-X30のうちスタックにpushしていたものをpopする。また、LR レジスタをpopする。
  5. RET命令を使って、関数を呼び出した側に戻る。

Upper-Case Revisited

// X0-X2 : Parameters to linux function servides
// X1 : address of output string
// X0 : address of input string
// X8 : linux function number

.global _start
_start:
    LDR X0, =instr
    LDR X1, =outstr
    BL toupper

    // print hex number
    MOV  X2, X0      // return code is the length
    MOV  X0, #1      // 1 = stdout
    LDR  X1, =outstr // string to print
    MOV  X8, #64     // linux write system call
    SVC  0           // call linux to output the string

    // exit the program
    MOV  X0, #0      // return code
    MOV  X8, #93     // Service comman code 93
    SVC  0           // call linux to terminate
.data
instr:    .asciz "This is out Test String that we will convert.\n"
outstr:   .fill  255, 1, 0
// X1 : address of output string
// X0 : address of input string
// X4 : original output string for length calc.
// W5 : current character being processed
.global toupper
toupper:
    MOV  X4, X1
loop:
    LDRB W5, [X0], #1   // load character and increment pointer
    CMP  W5, #'z'       // if W5 > 'z' goto cont
    B.GT cont
    CMP  W5, #'a'       // if W5 < 'a' goto cont
    B.LT cont
    SUB  W5, W5, #('a' - 'A')
cont:
    STRB W5, [X1], #1   // store character to output str
    CMP  W5, #0         // stop on hitting a null char
    B.NE loop
    SUB  X0, X1, X4
    RET
touper 関数は、leaf function (その実行中に他の関数を呼び出さない)なのでLRレジスタを保存していない。 X0-X18レジスタの値は関数実行中に変更しても構わない(=保存する必要ない)。

Stack Frames

4byte整数の変数を3個使う場合を考える。 単純にスタックを使った例
    SUB  SP, SP, #16
    STR  W0, [SP]       // store a
    STR  W1, [SP, #4]   // store b
    STR  W2, [SP, #8]   // store c
    ...
    ADD  SP, SP, #16
これだと、関数中でSPが変化すると、変数までのオフセットが異なるので困る。 そこで X29 を frame pointer (FP) として使う。
    SUB  FP, SP, #16    // この前にFPを保存する必要がある。また、MOV FP, SP でよいと思う。
    SUB  SP, SP, #16
    STR  W0, [FP]       // store a
    STR  W1, [FP, #-4]   // store b [自分へのメモ]これは変ではないか?FPを減らしているならば #4 だと思う。
    STR  W2, [FP, #-8]   // store c [自分へのメモ]これは変ではないか?FPを減らしているならば #8 だと思う。
    ...
    ADD  SP, SP, #16   // この後で FPを元に戻す必要がある
FPを使う場合は、関数の始めと終わりでスタックにPUSH/POPすること。 X29 (FP) は 16-byte aligned ではない。 この本では、FPは使わない方針である。 [自分へのメモ] だからか。この本はFPをの使い方の説明が正しくない可能性がある。関数開始時のSPの値をFPに記憶させて、局所変数には負のオフセットでアクセスする方が普通だと思う。

Defining Symbols

[自分へのメモ]FPを関数開始直後のSPの値を保存するように自分で変更した。元本の通りのコードではない。
    .EQU  VAR1, -4
    .EQU  VAR2, -8
    .EQU  SUM,  -12
SUMFN:
    STP  LR, FP, [SP, #-16]!
    MOV  FP, SP

    SUB  SP, SP, #16
    STR  W0, [FP, #VAR1]   // 1st parameter -> local variable
    STR  W1, [FP, #VAR2]   // 2nd parameter -> local variable

    LDR  W4, [FP, #VAR1]
    LDR  W5, [FP, #VAR2]
    ADD  W6, W4, W5
    STR  W6, [FP, #SUM]

    LDR W0, [FP, #SUM]
    MOV  SP, FP
    LDP  LR, FP, [SP], #16
    RET

Macros

.include "uppermacro.s"
.global _start
_start:
    // conver teststr
    toupper tststr, buffer
    // print
    MOV  X2, X0
    MOV  X0, #1
    LDR  X1, =buffer
    MOV  X8, #64
    SVC  0
    // conver teststr2
    toupper tststr2, buffer
    // print
    MOV  X2, X0
    MOV  X0, #1
    LDR  X1, =buffer
    MOV  X8, #64
    SVC  0
    // terminate
    MOV  X0, #0
    MOV  X8, #93
    SVC  0
.data
tststr:   .asciz  "This is our Test String that we will convert.\n"
tststr2:  .asciz  "This is a sedond string.\n"
buffer:   .fill  255, 1, 0
// uppermacro.s
// X1 : address of output string
// X0 : address of input string
// X2 : original output string for length calc.
// W3 : current character being processed
//
// lalbel_1 = looop
// labell_2 = cont
.MACRO   toupper   instr, outstr
    LDR  X0, =\instr
    LDR  X1,\outstr
    MOV  X2, X1
1:
    LDRB W3, [X0], #1  // load char and increment pointer
    CMP  W3, #'z'     // if letter > 'z'
    B.GT 2f           // goto end if
    CMP  W3, #'a'     // if letter < 'a'
    B.LT 2f           // goto end if

    SUB  W3, W3, #('a' - 'A')
2:
    STRB W3, [X1], #1 // store char and increment pointer
    CMP  W3, #0        // null terminate string
    B.NE 1b            // loop
    SUB X0, X1, X2
.ENDM

Include Directive

.include "uppermacro.s"

Macro Definition

.MACRO  macroname  parameter1, parameter2, ...

Labels

"loop" や "cont" のようなラベルは"1", "2"といったラベルに置き換えられる。 "2f"における"f"はforward方向の次のラベル"2"を意味する。 "1b"における"b"はbackward方向の次のラベル"1"を意味する。

Why Macros?

macroは展開されるので、コードは大きくなるが、性能は高くなることが期待される。

Macros to Improve Code

.MACRO  PUSH1 register
    STR  \register, [SP, #-16]!
.ENDM    
.MACRO  POP1 register
    LDR  \register, [SP], #16
.ENDM    
.MACRO  PUSH2 register1, register2
    STR  \register1, \register2, [SP, #-16]!
.ENDM    
.MACRO  POP2 register1, register2
    LDR  \register1, \register2, , [SP], #16
.ENDM    


http://nw.tsuda.ac.jp/