ARM Assembly에 대해 공부하던 중 좋은 글이 있어 번역 하였다.

출처: Azeria Labs (https://azeria-labs.com/writing-arm-assembly-part-5/)


값 불러오기 및 저장하기

가끔은 여러 숫자를 한번에 불러오거나 저장하는 것이 더 효율적일 때가 있습니다. 우리는 LDM(여러개 불러오기)와 STM(여러개 저장하기)를 그런 용도로 사용합니다. 해당 명령어 들은 시작 주소를 접근하는 방법만 다릅니다. 이 파트에서는 아래의 코드를 기반으로 설명할 것입니다. 아래에서 각 명령어 마다 스텝 바이 스텝으로 설명 하겠습니다.

.data

array_buff:
 .word 0x00000000             /* array_buff[0] */
 .word 0x00000000             /* array_buff[1] */
 .word 0x00000000             /* array_buff[2]. This element has a relative address of array_buff+8 */
 .word 0x00000000             /* array_buff[3] */
 .word 0x00000000             /* array_buff[4] */

.text
.global _start

_start:
 adr r0, words+12             /* address of words[3] -> r0 */
 ldr r1, array_buff_bridge    /* address of array_buff[0] -> r1 */
 ldr r2, array_buff_bridge+4  /* address of array_buff[2] -> r2 */
 ldm r0, {r4,r5}              /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
 stm r1, {r4,r5}              /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
 ldmia r0, {r4-r6}            /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
 stmia r1, {r4-r6}            /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
 ldmib r0, {r4-r6}            /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
 stmib r1, {r4-r6}            /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
 ldmda r0, {r4-r6}            /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
 ldmdb r0, {r4-r6}            /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
 stmda r2, {r4-r6}            /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
 stmdb r2, {r4-r5}            /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
 bx lr

words:
 .word 0x00000000             /* words[0] */
 .word 0x00000001             /* words[1] */
 .word 0x00000002             /* words[2] */
 .word 0x00000003             /* words[3] */
 .word 0x00000004             /* words[4] */
 .word 0x00000005             /* words[5] */
 .word 0x00000006             /* words[6] */

array_buff_bridge:
 .word array_buff             /* address of array_buff, or in other words - array_buff[0] */
 .word array_buff+8           /* address of array_buff[2] */

시작하기 전에, .word는 데이터(메모리)의 32 bits = 4 bytes 블록을 뜻합니다. 이것은 offsetting을 이해하는 데에 매우 중요합니다. 위의 프로그램은 .data 섹션에 5개의 원소를 가진 빈 배열(array_buff)를 선언했습니다. 우리는 이 공간을 데이터 저장하기 위한 작성 가능한 메모리 공간으로 사용할 것입니다. .text 섹션은 코드로 작성된 메모리 연산 명령과 읽기 전용 두 가지 레이블이 존재합니다: 하나는 7개의 원소를 가진 배열이고, 다른것은 .text와 .data 섹션을 이어주는(bridging) 것으로서 우리가 .data 섹션에서 array_buff에 접근 가능하게 해줍니다.

adr r0, words+12     /* address of words[3] -> r0 */

우리는 ADR 명령어를 사용해서 (게으른 방법) 4번째에 있는 word[3] 원소의 주소를 R0에 집어넣습니다. 단어 배열의 가운데를 지정한 이유는, 해당 위치에서 앞뒤로 연산을 할 것이기 때문입니다.

gef> break _start
gef> run
gef> nexti

R0는 현재 word[3]을 가리키고 있습니다. 해당 위치는 0x80B8 입니다. 이 말인 즉슨, 우리의 배열 시작점 즉 word[0]의 주소는 0x80AC 라는 것입니다 (0x80B8 - 0xC).

gef> x/7w 0x00080AC
0x80ac <words>: 0x00000000 0x00000001 0x00000002 0x00000003
0x80bc <words+16>: 0x00000004 0x00000005 0x00000006

우리는 R1과 R2를 배열의 첫번째 원소(array_buff[0])와 세번째 원소(array_buff[2])로 지정해 둡니다. 각각의 주소가 지정되고 나면, 우리가 주소들을 바탕으로 연산을 할 수 있습니다.

ldr r1, array_buff_bridge   /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */

위 두 개의 명령어를 실행 시키고 나면, R1과 R2는 array_buff[0]과 array_buff[2]의 주소를 가지게 됩니다.

gef> info register r1 r2
r1    0x100d0    65744
r2    0x100d8    65752

다음 명령어는 LDM을 사용해서 R0가 포인팅 하고있는 메모리 주소에 있는 두개의 워드(word) 값을 불러옵니다. 우리가 R0를 words[3]을 가리키게 했으므로, words[3]의 값이 R4에 저장되고 words[4]의 값은 R5에 저장됩니다.

ldm r0, {r4, r5}           /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */

2개의 데이터 블록을 하나의 명령어로 불러왔습니다. 그 결과 R4 = 0x00000003, R5 = 0x00000004로 설정된 것을 확인할 수 있습니다.

gef> info registers r4 r5
r4     0x3     3
r5     0x4     4 

이제 STM 명령어를 사용해서 여러 값을 메모리에 저장해 보겠습니다. 우리 코드 상의 STM 명령어는 레지스터 R4와 R5의 값을 가지고 가서(0x3과 0x4) R1이 가리키는 메모리 주소에 저장합니다. 우리가 이전에 R1을 array_buff를 가리키도록 설정해 뒀기 때문에, 이 명령어가 실행되고 나면 array_buff[0] = 0x00000003 과 array_buff[1] = 0x00000004가 됩니다. 만약 이렇게 특정돼 있지 않다면, LDM과 SDM은 1개의 word (32 bits = 4 byte) 만큼 움직일 것입니다.

stm r1, {r4, r5}          /* r4 -> array_buff[0]=0x03; r5 - array_buff[1]=0x04 */

해당 값인 0x3과 0x4는 메모리 주소 0x100D0과 0x100D4에 저장될 것입니다. 아래의 명령어를 통해 0x0001000D0에서 부터 두 개의 words 만큼을 출력해 봅시다.

gef> x/2w 0x000100D0
0x100d0 <array_buff>: 0x3 0x4

이전에 언급 했듯이, LDM과 STM은 여러 변종을 가지고 있습니다. 변종들은 명령어의 전치사를 통해 구분할 수 있습니다. 전치사의 예를 들자면: -IA (이후에 더하기), -IB (이전에 더하기), -DA (이후에 빼기), -DB (이전에 빼기) 등이 있습니다. 이러한 변종들은 어떻게 해당 명령어들이 첫 연산자를 기반으로 메모리에 접근 하는지를 표현합니다 (source address를 저장하고 있는 레지스터).

ldmia r0, {r4-r6} /* words[3] -> r4=0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */

위의 두 명령을 실행하고 난 후 레지스터 R4-R6과 메모리 주소 0x000100D0, 0x000100D4, 0x000100D8은 0x3, 0x4, 0x5를 값으로 가지게 됩니다.

gef> info registers r4 r5 r6
r4   0x3   3
r5   0x4   4
r6   0x5   5

gef> x/3w 0x000100D0
0x100d0 <array_buff>: 0x00000003 0x00000004 0x00000005

LBMIB 명령은 처음 source address를 4 bytes (= 1 word) 만큼 증가 시킨 후 첫 로드를 합니다. 이렇게 해섯 우리는 데이터를 순방향으로 읽을 수 있게 되지만, 첫번째 원소는 source address에서 4 byte 만큼 떨어져서 있게 됩니다. 이것이 우리의 예제에서 LDMIB 명령을 통해 메모리에서 R4로 읽어 들이는 첫 원소가 R0이 가리키는 0x00000003(words[3])이 아닌 0x0000004(words[4])인 이유입니다.

ldmib r0, {r4-r6}   /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6}   /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */

위 두 명령을 실행하고 나면, R4-R6 레지스터의 값과 0x100D4, 0x100D8, 0x100DC 위치에 저장돤 값이 0x4, 0x5, 0x6이 됩니다.

gef> x/3w 0x100D4
0x100d4 <array_buff+4>: 0x00000004 0x00000005 0x00000006

gef> info register r4 r5 r6
r4   0x4   4
r5   0x5   5
r6   0x6   6

LDMDA 명령의 경우는 모든 것을 반대로 연산합니다. R0을 words[3]으로 지정 하겠습니다. 만약 우리가 거꾸로 읽는다 가정했을 때 words[3], words[2], words[1] 을 R6, R5, R4에 불러오게 됩니다. 눈치 채셨듯이, 맞습니다. 레지스터도 똑같이 거꾸로 불러오게 됩니다. 따라서 명령어가 실행 완료 되면 R6 = 0x00000003, R5 = 0x00000002, R4 = 0x00000001 이 됩니다. 그 이유는 우리가 source address를 각각의 불러오기 행위 이후에 감소 연산이 진행되기 때문입니다. 거꾸로 읽기 행위가 일어나는 이유는 우리가 각각의 메모리 주소를 불러올 때 마다 감소 연산을 같이 진행 하기 때문에, 레지스트리 숫자도 줄어들면서 높은 메모리 주소가 높은 레지스트리 숫자를 가져야 한다는 논리를 따르기 때문입니다. LDMIA(혹은 LDM) 명령어 예제를 보면, 우리는 낮은 레지스트리를 먼저 부릅니다. 왜냐면 source address 가 낮기 때문이며, 그 후에 높은 레지스트리를 부르는데 왜냐면 source address가 증가했기 때문입니다.

여러 개를 불러온 후에 감소시키는 경우는:

ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */

위 명령 실행 후 레지스터 R4, R5, R6는:

gef> info register r4 r5 r6
r4    0x1    1
r5    0x2    2
r6    0x3    3

여러개 불러 오기 이전에 감소하는 경우는:

ldmdb r0, {r4-r6} /* words[2]->r6 = 0x02; words[1]->r5 = 0x01; words[1]->r4 = 0x01 */ 

위 명령 실행 후 레지스터 R4, R5, R6는:

gef> info register r4 r5 r6
r4    0x0    0
r5    0x1    1
r6    0x2    2

여러개 저장한 후 감소시키는 경우는:

stmda r2, {r4-r6} /* r6->array_buff[2] = 0x02; r5->array_buff[1] = 0x01; r4->array_buff[0] = 0x00 */

명령 실행 후 array_buff[2], array_buff[1], array_buff[0]의 메모리 주소는:

gef> x/3w 0x100D0
0x100d0 <array_buff>: 0x00000000 0x00000001 0x00000002

미리 감소 후 여러개 저장하는 경우는:

stmdb r2, {r4-r5} /* r5->array_buff[1] = 0x01; r4->array_buff[0] = 0x00; */

명령 실행 후 array_buff[1]과 array_buff[0]의 메모리 주소는:

gef> x/2w 0x100D0
0x100d0 <array_buff>: 0x00000000 0x00000001

PUSH와 POP

메모리 위치 중 스택으로 불리는 프로세스가 있습니다. 스택 포인터(SP)는 일반적인 상황에서 언제나 스택의 메모리 위치를 가리키는 레지스터 입니다. 어플리케이션은 주로 스택을 임시 데이터 저장소로 사용합니다. 이전에 언급 했듯이, ARM은 불러오기/저장하기 모델을 통해 메모리에 접근합니다. 이말인 즉슨 LDR / STR 명령 혹은 그들의 지시자(LDM.. / STM..)을 메모리 연산에 사용한다는 것입니다. x86에서는 PUSH와 POP을 스택에 값을 넣거나 뺄때 사용합니다. ARM에서 우리도 이 두개의 명령어를 사용할 수 있습니다.

우리가 완전 내림차순으로 뭔가를 스택에 PUSH 할때는(스택에 대해 다른점을 더 알고싶다면 Part 7: 스택과 함수 를 참고하세요) 아래와 같은 일이 일어납니다:

  1. 첫번째로, SP의 주소는 4만큼 줄어듭니다.
  2. 두번째로, SP가 가리키는 새로운 메모리 주소에 정보가 저장됩니다.

우리가 뭔가를 스택에서 POP할때는 아래와 같은 일이 일어납니다:

  1. 현재 SP의 주소를 특정 레지스터에 불러옵니다.
  2. SP의 주소가 4만큼 늘어납니다.

아래 예제에서는 PUSH/POP과 LDMIA/STMDB를 사용하고 있습니다.

.text
.global _start

_start:
   mov r0, #3
   mov r1, #4
   push {r0, r1}
   pop {r2, r3}
   stmdb sp!, {r0, r1}
   ldmia sp!, {r4, r5}
   bkpt

이 코드를 디스어셈블리해서 한번 봅시다.

azeria@labs:~$ as pushpop.s -o pushpop.o
azeria@labs:~$ ld pushpop.o -o pushpop
azeria@labs:~$ objdump -D pushpop
pushpop: file format elf32-littlearm

Disassembly of section .text:

00008054 <_start>:
 8054: e3a00003 mov r0, #3
 8058: e3a01004 mov r1, #4
 805c: e92d0003 push {r0, r1}
 8060: e8bd000c pop {r2, r3}
 8064: e92d0003 push {r0, r1}
 8068: e8bd0030 pop {r4, r5}
 806c: e1200070 bkpt 0x0000

위에서 볼 수 있듯이, LDMIA와 STMDB 명령어는 PUSH와 POP로 해석되었습니다. 왜냐하면 STMDB sp!, reglist는 PUSH와 동의어이고 LDMIA sp! reglist는 LDMIA와 동의어이기 때문입니다(참고: ARM 매뉴얼).

이 코드를 GDB에서 실행해 봅시다.

gef> break _start
gef> run
gef> nexti 2
[...]
gef> x/w $sp
0xbefff7e0: 0x00000001

위 두개의 명령어를 실행하고 난 뒤 SP가 어떤 메모리 주소를 가리키고 있는지 확인해 보겠습니다. 다음 PUSH 명령어는 SP를 8만큼 감소 시켜야 하며, R1과 R0의 자료를 나열한 순서에 맞게 스택에 저장하게 됩니다.

gef> nexti
[...] ----- Stack -----
0xbefff7d8|+0x00: 0x3 <- $sp
0xbefff7dc|+0x04: 0x4
0xbefff7e0|+0x08: 0x1
[...]
gef> x/w $sp
0xbefff7d8: 0x00000003

다음으로는, 이 두개의 값(0x3, 0x4)를 레지스터로 POP 해서 R2=0x3, R3=0x4가 됩니다. SP는 8만큼 증가합니다.