무엇이 되었든, Assembly를 다룬다는 건 Software Engineer로서는 상당히 강력한 무기를 가졌다고 할 수 있습니다. Assembly에만 익숙하다면 Software Debugging에도 유리하고, 심지어 Reverse Engineering도 할 수 있습니다. 그러면 가장 간단한 Assembly program 예제를 하나 들어서 Assembly는 어떻게 구성되는지 알아보도록 하겠습니다. Assembly Structure & Syntax라고나 할까요.
아래 예제는 간단한 Assembly 예제 인데요. 언제나 그랬듯이 Hello World 예제 입니다. 그다지 Hello World만 찍으면 심심하니까, 몇 가지를 조금 더 첨부해서 풍성한 이해를 도와야 겠지요. ㅋ . 직접 Assembly로 Hello World를 출력하는 예제를 만들어 보겠습니다.
Helloworld.s -----------------------------------------------------------------------
CODE32
AREA Helloworld CODE, READONLY
ENTRY
BEGIN ; LABEL
ADR r0, THUMB+1
BX r0
CODE16
THUMB ; LABEL
ADR r1, TEXT
LOOP ; LABEL
LDRB r0, [r1], #1
CMP r0, #0
BLNE print_ch
BNE LOOP
B EXIT
TEXT = "Hello World ", 0 ; TEXT → LABEL
END
----------------------------------------------------------------------------------
어때요? 간단해 보이나요? 그 속을 해부해 보져 머.
자 보니까 어떠세요? 뭐 여기저기 기웃거려 보면 Directive니 뭐 어쩌구 해서 어렵게 설명해 놓았습니다만, 크게는 Directive라고 불리는 것들과 Label, Instruction, 그리고 Comment로 나뉘죠. 간단하게 공식적인 syntax를 알아보시죠. (Directive 위주로)
AREA
일단 Code의 모음을 하나의 AREA로 묶을 수 있습니다. 이런 AREA는 어떤 assembly의 block의 속성을 정해 줄 수 있습니다. 그러니까 이게 CODE인지, DATA인지 또는 READONLY인지 READWRITE 인지 등을 정해서 묶음을 만들 수 있습니다. 이런 AREA는 이름을 가지며, 이 이름을 근거로 Scatter Loading등에서 메모리에서의 위치를 지정할 수도 있습니다. 조금 더 유식하게 말한다면 ELF file format에서 Section이라고 보시면 되고, linker가 다루는 단위 중 최소 단위라고 보시면 됩니다. 나눌 수 없는 거죠. Linker는 이 AREA단위로 주소를 할당한다고 보시면 됩니다. 결국엔 elf format의 section과 같다고 보시면 됩니다. AREA를 만들면서 줄 수 있는 속성은,
ALIGN : alignment. default 4 byte
CODE : contain machine instruction.
DATA : contain data
COMDEF : common section definition
COMMON : common data section
NOINIT : uninitialized data section or initialized to zero
READONLY : Read only section
READWRITE : Read/ Write section
이에요.
ENTRY
AREA에 ENTRY가 따라 붙으면 AREA에서 수행되어야 할 첫 번째 위치를 가리킵니다. 보통은 난생처음 진입하는 곳에 이런 것이 따라 붙는데, 제일 먼저 pc가 어디를 가리켜야 하는지를 모르니까 그걸 알려준다고 생각하면 됩니다.
CODE32/ CODE16
Assembler에게 CODE32를 만나면 32bit ARM code로 CODE16을 만나면 16bit Thumb code로 되어 있다고 알려주는 지시어 에요. 그러니까, 직접 C 컴파일러로 파일 하나를 컴파일 하면 16bit Thumb code나 32bit ARM code로 밖에 못 만드는데, Assembly code로 직접 짜면 파일 하나 안에도 16 bit Thumb code와 32 bit ARM code를 혼재 시킬 수 있다는 의미 이지요.
LABELS : BEGIN/ THUMB/ LOOP/ TEXT
BEGIN/ THUMB/ LOOP/ TEXT는 label로서 코딩 하는 사람 맘대로 이름 붙인 이름표 입니다. 이름표를 붙여놓으면 그 이름표 자체가 Symbol의 의미가 되어 이름표 = Label이 있는 곳의 주소를 의미하게 됩니다. 그래야 Data를 가리키던, 어디론가 branch (Jump)를 하든 할 것 아니겠습니꺄? ㅋ
END
Assembly로 짜여진 file의 끝을 의미합니다. File이 끝났다고 Assembler에게 알려주는 거지요. 후후.
뭐 대충의 구조 얘기를 끝냈으니, 어떤 Assembly 내용인지 다시 한번 자세히 한번 보시겠사옵니까? Assembly에서 comment는 ; 으로 달고요 그 응용으로 예시의 Assembly에 그 설명들을 달아 보았습니다 깜찍할까 몰라.
Helloworld.s -----------------------------------------------------------------------
CODE32 ; ARM mode로 짰삼.
AREA Helloworld CODE, READONLY ; 이 코드 block의 이름과 속성, 이름은 HelloWorld
ENTRY ; Instruction이 제일 처음 실행할 곳.
BEGIN
ADR r0, THUMB+1 ; r0에 THUMB label의 주소를 넣음.
BX r0 ; r0값으로 점프
CODE16 ; 여기서부터는 THUMB mode로 컴파일 해줘.
THUMB
ADR r1, TEXT ; r1 ← "Hello World"의 주소
LOOP LDRB r0, [r1], #1 ; r0에 r1가 가르키는 주소에 들어 있는 내용을 1byte 만큼 Load한 후 r1의 값을 1 증가해야 해.
CMP r0, #0 ; 읽어들인 값이 0인지 비교하여 끝인지 확인하자.
BLNE print_ch ; printf_ch로 갔다 오자.
BNE LOOP ; 만약 0이 아니면 다시 하나 읽어들이기 위해 LOOP로 닥치고 돌아가
B EXIT ; 0이면 EXIT로 jump
TEXT = "Hello World ", 0 ; Data
END ; Assembly file의 끝이여.
어때요. 대충 감이 오시죠. 뭐 간단합니다. 크흑 역사적인 Hello world가 탄생하는 순간입니다. 처음부터 보시면 ENTRY가 있으니까 BEGIN부터 시작하고요, THUMB으로 branch해서 Hello world의 주소를 가져다가 1byte씩 장난쳐서 출력하는 프로그램 입니다. 그런데, 여기서 한가지 THUMB으로 branch할 때 THUMB+1을 해주는 이유는 뭘까요? THUMB mode와 ARM mode는 어차피 2byte와 4byte이기 때문에 시작 주소가 짝수일 수 밖에 없으니까 2byte짜리 Thumb과 4byte ARM의 구분을 branch 할 때 branch할 target주소의 끝이 홀수면 Thumb, 짝수면 ARM으로 구분합니다. 실제로는 Thumb mode로 branch 하고 난 후 홀수지만 마지막의 1을 뺀 짝수 값에서 실행을 하고요, 그러니까 0x1001로 branch를 한다면, 0x1000으로 Thumb mode로 실행한다는 의미고요, CPSR의 ARM/Thumb bit만 Thumb으로 setting해 준답니다. 자세한 얘기는 ARM과 Thumb mode의 Veneer에서 하고요, 그냥 그렇다고 넘어가시죠.
자자, 그러면 실제로 Hello world를 C file로 짠 후, 그걸 Assembler로 만들면 이거랑 비슷하게 나올까요? 절~대~로 그렇지는 않습니다~ 왜냐하면~ 위의 예제는 순전히 설명을 위해서 가라로 만든 코드니까요. 아래의 Hello world C code를 한번 Assembly로 만들어 봅시다. 실제로는 뭐가 다른가. 일단은 이런 코드들을 보면서 공부를 하면 더~욱 금방 Assembly에 익숙해 질 수 있습니다. 우선은 Hello world의 C code는 다음과 같습니다.
helloworld.c -------------------------------------------------------------------------------
void hello ()
{
char *data;
const char text[]="Hello world";
data = (char *)text;
printf (" %s ", data);
return;
}
간단하지유? 누가 봐도 알만한 Hello World 프로그램 입니다. 이 녀석을 ARM mode로 컴파일 해볼까요? 우리 이미 배운 Assembly까지만 만드는 -S option으로 한번 만들어 보겠습니다.
armcc -o helloworldarm.s -S helloworld.c
; generated by ARM C Compiler, ADS1.2 [Build 805]
; commandline [-O2 -S -IC:\apps\ADS12\INCLUDE]
CODE32
AREA ||.text||, CODE, READONLY
hello PROC
|L1.0|
STMFD sp!,{r1-r3,lr}
MOV r0,sp
MOV r2,#0xc
LDR r1,|L1.36|
BL __rt_memcpy
MOV r1,sp
ADR r0,|L1.40|
BL _printf
LDMFD sp!,{r1-r3,pc}
|L1.36|
DCD ||.constdata$1||
|L1.40|
DCB " %s "
DCB "\0\0\0\0"
ENDP
AREA ||.constdata||, DATA, READONLY, ALIGN=0
||.constdata$1||
DCB 0x48,0x65,0x6c,0x6c
DCB 0x6f,0x20,0x77,0x6f
DCB 0x72,0x6c,0x64,0x00
END
오 비슷한 듯 하면서 비슷하지 않은 code가 만들어 졌네요. 일단은~ 코멘트를 통해서 하나 하나 살펴 보시도록 하시시십다.
CODE32 ; 자, 32bit arm mode라는 얘기구요.
AREA ||.text||, CODE, READONLY ; linker의 최소단위인데 이름은 ||.text.||이고
CODE속성에 READONLY임다
hello PROC ; 함수의 시작인데 이름은 hello 에요.
|L1.0| ; 레이블이고요. 누군가가 이 주소를 참조하나 보죠?
STMFD sp!,{r1-r3,lr} ; 일단 stack에 r1~r3하고 lr을 backup하고 시작합시다.
MOV r0,sp ; 요기서부터는 printf를 사용하기 위한 setting들이에요.
MOV r2,#0xc ;
LDR r1,|L1.36| ; |L1.36| 레이블에 있는 ||.constdata$1||을 r1에 load하고요.
; 요게 Hello World
; 저기~ 뒤에 AREA가 하나 더 있네요? READONLY data를 위한.
BL __rt_memcpy ; 일단은 text를 data에 복사하기 위한 함수로 갔다 옴다
MOV r1,sp
ADR r0,|L1.40| ; r0에는 |L1.40|레이블의 주소를 넣습니다.
BL _printf ; printf로 갔다 오네요.
LDMFD sp!,{r1-r3,pc} ; 그리고 위로 돌아가기 위해서 stack에 넣었던 값들을 복원합니다.
; lr을 pc에 집어 넣으면 원래 호출되었던 장소로 돌아갈 수 있다는 의미 인데요,
; 이건 뒤에 함수의 호출과정을 자세히 다루니까, 그때 또 자세히 봐요.
|L1.36|
DCD ||.constdata$1||
|L1.40|
DCB " %s "
DCB "\0\0\0\0"
ENDP ; hello PROC의 끝을 알립니다.
AREA ||.constdata||, DATA, READONLY, ALIGN=0
||.constdata$1||
DCB 0x48,0x65,0x6c,0x6c
DCB 0x6f,0x20,0x77,0x6f
DCB 0x72,0x6c,0x64,0x00
END
몇가지 syntax를 본다면..
함수이름 PROC
함수가 시작된다는 의미이고, 함수의 이름이 앞에 주어집니다.
ENDP
함수가 끝난다는 의미에요.
직접 짠 거는 printf를 안 써서 좀 어렵게 짰지만, 막상 compile해서 나온 넘이랑 Idea자체는 비스므리 합니다 - 라고 해 놓고 별로 그렇지 않은데 하고 갸우뚱 하는 분들도 있을 거라는 생각이 듭니다-. 비스므리 안 하더라도 Assembly에 더 익숙해 진다고 보면 마냥 valueless하지만은 않을 걸요. 우선 차이점은 standard library인 printf를 사용하고, hello()함수는 ENTRY없이 누군가가 불러 쓸 거라는 가정으로 compile하니까 stack관련한 처리도 존재하고, hello PROC하고 ENDP도 사용하게 되네요.
어쨌거나, 이런 식으로 ARM assembly는 구성되니까, 그 구성을 잘 이해 하시면 Reverse Engineering도 금방이라니까요. 하악하악.
또 한가지, Assembly를 설명하면서 * (레지스터이름) 등의 pointer 형식의 설명을 쓸 때가 더러 있는데, 이건 레지스터이름의 값은 주소이고 이 주소가 가리키는 곳의 값을 의미합니다.
이게 편할 때도 있어 가끔 사용하니까 주의해 주세요.
지금까지는 ADS를 기준으로 Assembly를 설명 했습니다만, 요즘은 Linux나 OpenOS가 주목 받고 있는 추세라 잠시 GNU Assembly를 짚고 넘어가야겠습니다. 실제 Assembly는 Register가지고 장난치는 거니까 같다고 보시면 되고요. 다른 점은 Directive가 다르다는 점이에요.
뭐, 뭐 GNU 얘기는 잠시 집어 치우고, ADS에서 잘 쓰는 몇가지 Directive들을 늘어놔 볼게요.
ALIGN
: 이 Directive를 만나면 이 이후부터는 ALIGN의 bit 수 만큼씩 compiler가 정렬을 해주지요
EQU
: C에서의 #define하고 똑같사옵니다.
#define SDRAM_BASE 0x20000000
SDRAM_BASE EQU 0x20000000
똑같은 표현이에요.
MACRO
: 내 사랑 Macro네요. 역시나 Assembler에서도 Macro를 만들 수 있겠습니다. (내부에서는 $라는걸로 처리하는데요.)
MACRO로 시작해서 MEND로 끝나면 되는데요, 마치 C의 함수처럼 인자를 받을 수도 있습니다.
MMU의 Page able base를 set하는 Assembly Macro를 만들어 본다면,
MACRO
mmu_page_table $address
ldr r0, = $address mcr p15, 0 , r0, c2, c0, 0
MEND
요런식으로 만들 수 있겠사옵니다. 부를 때는
mmu_page_table 0x80000
뭐 이런식으로 불러쓰면 MACRO가 불리우면서 $address를 0x80000으로 처리해 줘요.
뭐~ 대충 이런식인거죠.
댓글