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