본문 바로가기
ARM, Thumb mode와 S 접미사

THUMB mode는 왜 태어 났는가? THUMB mode는 도대체 무엇을 하는 녀석이란 말인가? THUMB mode는 16 bit 라는데 그건 또 무엇인가? 하는 의혹을 파헤쳐 보는 시간. THUMB mode는 어찌 보면 ARM mode의 반쪽 version이라고 볼 수 있습니다. 원래 ARM은 32bit RISC machine이고, 32bit로 동작하는 게 최상의 Performance를 제공할 수 있다는 거죠. machine마다 word size가 다른 건 바로 그런 의미입니다. word란 CPU가 한번에 처리할 수 있는 크기를 말하니까요. 예를 들어 8 bit apple은 8 bit를 word 단위로 하고, 8bit씩 처리를 합니다. 16bit INTEL AT는 16 bit를 word 단위로 하고, 16 bit씩 처리를 하지요. 같은 말로 32 bit ARM은 32가 word 단위이고 한번에 32bit씩 처리할 수 있다는 말입니다요.  바야흐로 THUMB mode는 32bit ARM에서 돌아가는 16 bit 기계어 라는 말인데, 왜 32 bit machine에서 16bit THUMB mode가 필요한 걸 까요? 그 답은 바로 Business에 있습니다. 처음 32 bit ARM을 만들어 냈을 때, 이 세상을 지배하던 Embedded system의 memory는 16 bit data line을 가진 Memory가 그 시대의 주인공이었슴다. 32 bit Core라고 해서 그 시대에 가장 흔히 구할 수 있고 많이 생산되던 16bit bus line의 메모리를 무시할 수 없는 노릇이었지요.  결국 ARM사는 16 bit bus line을 가진 Memory에서도 효율적으로 사용할 수 있도록 ARM 명령어들을 16 bit로 압축한 명령어 set을 발표하는데 그것이 ARM의 일부분인 THUMB인 것입니다.  그러니까, Thumb mode는 무조건 16bit 명령어 체계이고요, ARM 명령어는 무조건 32bit 명령어 체계 이지요? 그러니까 명령어들의 주소를 보면 Thumb은 2의 배수로, ARM은 4의 배수로 되어 있어요. 그러면, 이렇게도 생각할 수 있겠지요. Thumb은 ARM의 압축된 형태이다. 하지만 그냥 압축된 형태는 아니고, ARM 명령어 중에 가장 많이 쓰이는 녀석들을 모아서 Thumb 명령어 set을 다시 16bit로 만들어 낸 겁니다. Thumb을 쓰려거든 ARM 명령어 중에서 어느 정도는 포기해야 하는 명령어들이 생긴 거지요. 그렇다고 완전 포기하는 건 아니고, ARM에서 한 줄이면 될 것을 가지고 Thumb으로 하려면 몇 줄이 되는 뭐 그런 case들이 생긴 거에요.  뭐, 쉽게 얘기하면 ARM core를 팔아 먹으려고 보니, 세상이 따라오질 못하고 있더라~ 해서 그 시대에 맞는 16bit Data bus에도 Performance가 꽤나 괜찮은 기계어 set을 만들어 발표 했는데 그 녀석이 Thumb이라는 이야깁죠.  자, 그럼 예제 프로그램 하나를 만들어서 ARM mode와 Thumb mode로 컴파일 해서 무엇이 다른지 살펴 보도록 하겠습니다. 계산식도 포함하고, 조건문도 포함한 간단한 녀석으로 하나 해보죠.

void egARMThumb ()
{ int loop=0; int sum=0;
   for (loop=0; loop<10; loop++)
   {  sum = sum + loop; 
       printf ("%d\n", sum);  
       if (sum > loop)   break; 
   } 
       return ;
}

먼저 ARM mode로 compile된 녀석을 살펴 보면, 다음과 같습니다.

armcc -o egARM.s -S egARMThumb.c
-------------------------------------------------------------------------------------
; generated by ARM C Compiler, ADS1.2 [Build 805]
; commandline [-O2 -S -IC:\apps\ADS12\INCLUDE]        CODE32
        AREA ||.text||, CODE, READONLY
egARMThumb 
PROC                      ; 함수이름은 egARMThumb 1       
STMFD    sp!,{r3-r5,lr}       ; Stack에 r3~r5, lr을 집어 넣고 2       
MOV      r5,#0                   ; r5:=0, 즉 sum = 0 3       
MOV      r4,#0                   ; r4:=0, 즉 loop = 0|L1.12|                                   
                                       ; 지맘대로 Label하나 정해 놓고 4       
ADD      r5,r5,r4                 ; r5:=r5+r4  sum = sum + loop 5       
MOV      r1,r5                    ; printf 를 부르기 위한 준비작업 6       
ADR      r0,|L1.48|              7       
BL       _printf                    ; printf를 부름. 8       
CMP      r5,r4                     ; sum과 loop를 비교해서  9       
ADDLE    r4,r4,#1                ; 비교 결과가 sum이 loop보다 작거나 같으면 loop에 1을 더하고 10     
CMPLE    r4,#0xa                ; 비교 결과가 sum이 loop보다 작거나 같으면면 loop가 10보다 큰지 확인 11     
BLT      |L1.12|                  ; 10보다 더 작으면 |L1.12|로 jump해서 다시 for를 계속 12     
LDMFD    sp!,{r3-r5,pc}       ; 함수의 끝. 그러니까 sum이 loop보다 크고,
                                               ; sum이 10보다 크면 여기로 곧바로 내려옴. 
                                               ; Stack에 저장했던 값을 복원하고 lr값을 pc에 넣고 돌아감~|L1.48|        
DCB      "%d\n\0"        
ENDP                                        ; 함수 구현의 끝.

Thumb mode는 또한 다음과 같지요.

tcc -S -o egthum.s -S egARMthumb.c
-----------------------------------------------------------------------------------------

; generated by Thumb C Compiler, ADS1.2 [Build 805]
; commandline [-O2 -S -IC:\apps\ADS12\INCLUDE]        CODE16
        AREA ||.text||, CODE, READONLY
egARMThumb 
PROC                        ; 함수이름은 egARMThumb이다. 1       
PUSH     {r4,r5,r7,lr}            ; Stack에 r4, r5, r7, lr을 넣고 2       
MOV      r4,#0                     ; r4:=0 즉,  loop = 0  3       
MOV      r5,#0                     ; r5:=0 즉., sum = 0
|L1.6|                                        ; 지맘대로 Label하나 만들고 4       
ADD      r5,r5,r4                   ; r5:=r5+r4 즉, sum = loop + sum 5       
MOV      r1,r5                      ; printf 를 부르기 위한 준비 6       
ADR      r0,|L1.28| 7       
BL       _printf 8       
CMP      r5,r4                       ; r5와 r4를 비교해서 (sum과 loop를 비교해서) 9       
BGT      |L1.26|                    ; sum이 더 크면 |L1.26|으로 jump해서 for를 끝냄. 10     
ADD      r4,#1                       ; loop에 1을 더함 11     
CMP      r4,#0xa                   ; loop가 10보다 큰지 확인 12     
BLT      |L1.6|                      ; loop가 10보다 작으면 |L1.6|로 jump|L1.26| 13     
POP      {r4,r5,r7,pc}            ; 함수의 끝
                                                 ; Stack에서 r4, r5, r6을 복원하고 pc에 lr을 넣고 돌아감.
|L1.28| DATA        DCB      "%d\n\0"        
ENDP                                    ; 함수 구현의 끝.

한번 슬쩍 둘러봐 보세요. 무슨 차이가 느껴지지 않습니까? 1. ARM mode가 Thumb mode보다 1줄 짧다. 일단 ARM mode가 Thumb mode보다 날씬~ 압축된 느낌 아닌가요?  ARM은 12 line, Thumb은 13line으로 구성되어 있습니다.   2. 뭔가 비교하는 게 틀리다.     Thumb은 조건을 CMP로 비교 후, 그 결과를 가지고 뭔가를 판단하는데 비해 ARM은 앞의 산술 결과를 계속 이용할 수 있으며 모든 명령어가 조건부 실행이 가능하게 구성되어 있어요. 3. Stack 명령어가 다르다.    ARM은 Multiple Register Transfer 명령어로 되어 있는 반면 Thumb은 PUSH와 POP으로 이루어져 있습니다.  앞의 3가지가 가장 큰 다른 점이라고 보겠습니다. 1번의 경우에는 그럴 수도 있고 아닐 수도 있는데 대부분 ARM mode로 구현된 경우가 Thumb mode로 구현된 것 보다 instruction 개수는 더 적습니다. 왜냐하면 ARM mode는 항상 조건부 실행이 가능하기 때문이죠. 뭐 이유로는 분기명령의 사용을 줄여 Pipe line에서 cancel 되는 횟수를 줄여 CPU가 노는 낭비를 줄일 수가 있지요. 그러다 보니 비교하는 방법이 틀려지는 게 당연한 거죠. ARM 명령어에서는 명령어가 하나 실행 될 때 CPSR의 [31:28]의 4bit를 조건이 걸려 있으면 조건 확인이 가능하고 그 조건에 따라 명령의 실행 여부를 결정할 수 있습니다. 만약 ARM 명령어에 조건이 걸려 있지 않으면 always라는 상태로서 조건에 상관없이 실행되도록 하지요. 앞에서 CPSR쯤에 이런 말을 한 적이 있습니다.  " ARM core는 Opcode를 Memory에서 가져오자 마자 (Fetch) 이를 무조건 실행하는 것이 아니라 condition flag인 NZCV를 보고 바로 앞 opcode의 실행결과를 보고 실행할지 말지를 결정할 수 있어요. Default는 AL "Always", condition과 관계 없이 항상 실행 이긴 하지만요. "뭐 이 말이 여기에 어울리게 될 줄은 상상도 못했지요? ㅋ 그러니까 ARM 명령어는 조건부로 실행할 수 있지만 Thumb 명령어는 조건부 bit가 없으므로 무조건적으로 실행된다니까요.  여러 가지 많은 경우가 있지만, 가장 많이 쓰이는 몇 가지만 짚고 넘어 갈게요. CPSR의 flag를 보고 결정하지만, 보통 바로 앞의 연산 결과라는 게, 비교 즉, cmp하는 건데, CMPS r0, r1이라고 함은 r0-r1을 해서 그 결과를 보게 되는 거에요. 그 결과는 CPSR의 NZCV에 update되니까 그걸 참조하면 됩니다. (여기에서 S는 연산결과를 CPSR의 NZCV에 update해라라는 뜻이에요) 여기에서 대문자는 1, 소문자는 0으로 편리하게 포기 하겠습니다. Nz 하면 N=1, Z=0 이라는 뜻이에요.  EQ 는 EQUAL 같으면, 즉 그러니까 결과가 0이면, ZNE 는 NOT EQUAL 다르면, 즉 그러니까 결과가 0이 아니면, zGE 는 Greater than or Equal 크거나 같으면, 즉 그러니까 결과가 양수이거나, 음수이면서 overflow가 나거나 NV or nvGT 는 Greater than 크면, 즉 그러니까 결과가 양수이거나, 음수이면서 overflow가 나고, 0이 아니면 NzV or nzvLE 는 Less Than or Equal 작거나 같으면, 즉 결과가 0이거나, 음수이거나, 양수이면서 overflow가 났거나. Z or Nv or nVLT 는 Less Than 작으면, 음수이거나, 양수이면서 ovefflow가 났거나. Nv or nV  eg.) BGT : 앞의 결과가 크거나 같으면, Branch하고 아니면 말고. 이에요. 나머지 많은 것들은 아래 Table을 참고 하세요.

BEQ LABEL : Zero flag가 설정되면, LABEL로 분기한다. MOVCS r3, r5 : Carry flag가 설정되었으면, 레지스터 r3로 레지스터 r5의 내용을 가져온다. MOVNE r2, r4 : Zero flag가 설정되지 않았으면, 레지스터 r2로 레지스터 r4의 내용을 가져온다. ADDVS r0, r1, r2 : 오버플로우(V flag가 설정)가 일어나면 레지스터 r1과 레지스터 r2의 내용을 더해서 레지스터 r0에 저장한다.  마지막 3번의 경우는 뭐, Thumb mode에서는 stack 전용 명령어인 PUSH POP을 지원한다 정도이지, 특별히 다른 건 아닙니다. Multiple Register Transfer인 STMFD나 LDMFD를 Stack에 사용하는데 16bit Instruction에 다 넣으려면 너무 기니까 Thumb mode에서는 Stack 전용 명령어를 만들어 놓았다 정도로 이해하시면 됩니다.  아래 예가 나오니까, 확실히 ARM과 Thumb 차이를 구분하실 수 있을 거에요. 그러면, ARM mode로 구현된 함수와 Thumb mode로 구현된 함수 사이는 어떻게 서로 왔다 갔다 할 수 있을까?BX 명령어를 이용해서 왔다 갔다 할 수 있습니다. BX 명령어를 이용하면 compiler가 알아서 ARM ↔ Thumb 를 왔다 갔다 할 수 있도록 자동으로 연결 부위를 만들어 줍니다. 이때 자동으로 왔다 갔다 하는 부분은 ATPCS(ARM Thumb Procedure Call Standard)라고 부르는 약속된 룰에 의해서 Compiler가 잘~ 만들어 줍니다.  마지막으로 기계어로 생성된 ARM/ Thumb mode에 대한 비교를 보면 일단 Address를 보면 ARM은 4 byte (32bit씩), Thumb는 2byte (16bit씩)을 잡아 먹고 있는 걸 보실 수 있겠습니다.  

ARM mode
    .text
       Address           Mnemonic               Assembly        
0x00008000:    e92d4038    8@-.    STMFD    r13!,{r3-r5,r14}        
0x00008004:    e3a05000    .P..    MOV      r5,#0        
0x00008008:    e3a04000    .@..    MOV      r4,#0       
0x0000800c:    e0855004    .P..    ADD      r5,r5,r4        
0x00008010:    e1a01005    ....    MOV      r1,r5        
0x00008014:    e28f0014    ....    ADD      r0,pc,#0x14 ; #0x8030        
0x00008018:    eb000005    ....    BL       _printf  ; 0x8034        
0x0000801c:    e1550004    ..U.    CMP      r5,r4        
0x00008020:    d2844001    .@..    ADDLE    r4,r4,#1        
0x00008024:    d354000a    ..T.    CMPLE    r4,#0xa        
0x00008028:    bafffff7    ....    BLT      0x800c        
0x0000802c:    e8bd8038    8...    LDMFD    r13!,{r3-r5,pc}    $d        
0x00008030:    000a6425    %d..    DCD    680997
 
Thumb mode
    .text
       Address           Mnemonic         Assembly        
0x00008000:    b5b0        ..      PUSH     {r4,r5,r7,r14}        
0x00008002:    2400        .$      MOV      r4,#0        
0x00008004:    2500        .%      MOV      r5,#0       
0x00008006:    192d        -.      ADD      r5,r5,r4        
0x00008008:    1c29        ).      MOV      r1,r5        
0x0000800a:    a004        ..      ADR      r0,0x801c    $b        
0x0000800c:    f000f808    ....    BL       _printf  ; 0x8020        
0x00008010:    42a5        .B      CMP      r5,r4        
0x00008012:    dc02        ..      BGT      0x801a        
0x00008014:    3401        .4      ADD      r4,#1        
0x00008016:    2c0a        .,      CMP      r4,#0xa        
0x00008018:    dbf5        ..      BLT      0x8006       
 0x0000801a:    bdb0        ..      POP      {r4,r5,r7,pc}    $d        
0x0000801c:    000a6425    %d..    DCD    68099

여기서 끝내면 아쉬우니까, ARM/ Thumb mode의 차이 중 조건부실행에 대해서 좀 더 알아보면, "ARM Developer Suit Guide Version1.2 Assembler Guide"에 보면, 이런 예제가 나와 있습니다. 꽤나 유명한 예제인데, 한번 예를 (끄응) 들어 볼게요. 이제는 특별하게 코드 한줄 한줄 설명 안 해도 잘 아시겠죠.


C code로 아래와 같은 코드가 있다고 칩시다.
 
int gcd (int a, int b)
{
    while (a!= b)
   {
      if (a>b)
            a = a - b;
      else
            b = b - a;
    }
   return a;
}
 
이걸 Thumb mode로 짠다면 다음과 같이 되겠죠. 상당히 읽기 쉬운 코드 에요. 참고로 r0 = a이고요, r1 = b에요.
 
     CODE16
     AREA example, CODE, READONLY
gcd  PROC
        CMP    r0, r1
         BEQ    end
         BLT    less
         SUB    r0, r0, r1
         B        gcd
less  
        SUB      r1, r1, r0
        B         gcd
end
      ENDP
 
이걸 ARM mode로 짠다면, 어랏 간단하네. 단 네줄로 요로코롬 해결 가능하구만?
 
     CODE32
     AREA example, CODE, READONLY
gcd PROC
         CMP       r0, r1
         SUBGT   r0, r0, r1           ; r0가 r1보다 크면 r0-r1한다.
         SUBLT    r1, r1, r0          ; r0가 r1보다 작으면 r1-r0를 한다.
         BNE       gcd                  ; r0와 r1이 같지 않으면 gcd처음으로 간다.
     ENDP
 
ARM mode의 조건부 실행에 대한 정확한 예제가 아닌가 싶네요. 어때요. 이제 차이를 확실히 아시겠지요? 참고로 위의 두 mode에 대한 Assembly중 stack에 관련한 code가 없는 이유는 argument로 들어온 두 값으로 장난치고, a값. 즉 r0를 return 하면서, sub로 함수를 부르는 일이 없으므로 -이런 함수를 더 밑에 부르는 녀석이 없다고 하여 leaf 함수라고 부릅니다-

굳이 stack에 lr을 넣을 필요가 없는 거죠. 어때요. 이제 확 감 따러 갑니까?

 

MOVS 명령을 사용할 때 Rd가 R15인 경우에는 SPSR의 값이 CPSR로 복사가 되어 Exception 복구 명령에 사용되기도 한답니다. (Exception 쪽 보시고 난 후엔 아하~ 하실 거에요. ) 그런데, 이미 Register를 공부할 때, User mode나 System mode에는 SPSR이 없으니까, User mode나, System mode에서 MOVS를 사용하게 되면 이상동작을 할 수도 있으니까, 조심하셔야 해요.

 
 
Linked at 친절한 임베디드 시스템 개발자.. at 2009/10/01 12:32

... ⓒ ARM Thumb mode와 S 접미사 ... more

친절한 임베디드 시스템 개발자 되기 강좌 글 전체 리스트 (링크) -



댓글





친절한 임베디드 개발자 되기 강좌 글 전체 리스트 (링크) -