본문 바로가기

My Study/Programming&Theory

ARM 명령어 공부

ipad2에는 A5라는 CPU가 탑제되어 있습니다 


ARM 회사에서 만들었기 때문에 리버싱을 하려면 당연히 ARM CPU에 대해서 적당한 지식이 있어야합니다.
여기선 조금씩 알아보고 더욱 자세한 내용은 ARM CPU 문서들을 참고해주세요~

ARM CPU는 내부적으로 32Bit의 데이터 버스와 32Bit의 어드레드 버스를 제공합니다.
또한 잘 알려져 있듯이
ARM은 대표적인 RISC 코어입니다.

RISC 설명
http://terms.co.kr/RISC.htm 

즉, ARM의 명령어는 그 종류가 적으면서도, 다양하게 적용시킬 수 있는 특징이 있습니다.
하나씩 알아보면서 느껴보도록 하죠.

ARM 구조
1. 레지스터
 ARM은 31개의 32Bit 범용 레지스터가 있습니다. 또한 동작모드에 따라는 6개의 Status 레지스터도 있습니다.

2. ALU
 32Bit 연산이 가능한 ALU가 제공됩니다. ARM의 ALU 장점으로는 보통 다른 CPU에서는 shift 명령이 따로 있었는데, ARM에서는 따로 존재하는 것이 아닌 대부분의 명령에서 옵션으로 적용시킬 수 있습니다. ( Barrel Shifter )

3. Booth's 곱셈기
 곱셈 기능을 제공하는 32Bit Booth's 곱셈기가 있습니다. 곱셈기는 32Bit 연산을 지원하며, 32Bit의 두 입력을 받아서 곱하여, 결과가 32Bit를 넘더라도 넘는 부분은 버리고 32Bit만을 남깁니다. 

ARM의 레지스터
 위에서 모두 32Bit인 31개의 범용 레지스터와 6개의 상태 레지스터가 있다고 했습니다.
하지만 ARM 어셈블러에서 사용하는 범용 레지스터 키워드는 r0~r15까지 16개밖에 되지 않습니다. 즉, 다시 말해서 사용자가 한번에 사용할 수 있는 레지스터는 16개입니다. 그 중에 몇개는 프로그램 카운터(PC) 스택 포인터(SP) 등의 용도로 사용됩니다. 그러면 그 나머지는?? 나머지 레지스터는 CPU 동작모드와 관련되어 r0~r15로 리-맵핑되어 사용됩니다.

1. Special Purpose General Register
 위에서 사용자가 프로그램 할 때 레지스터 지정을 위해 사용할 수 있는 키워드는 r0~r15 입니다. 그 중에 특별한 목적을 위해 사용되는 레지스터를 알아보겠습니다. 

 - Program Counter ( r15 ) 
r15는 다른 CPU에서와 마찬가지로 PC 와 같은 역할을 합니다. 다만 차이점이 있다면 r15를 일반 다른 레지스터처럼 오퍼랜드로 사용할 수 있다는 점이고 ARM 어셈블러에서는 pc라는 키워드와 r15를 동일하게 취급합니다.
 * Intel CPU 어셈블러에서는 PC와 같은 역할을 하는 EIP 레지스터를 오퍼랜드로 사용할 수 없습니다. 

 - Stack Pointer ( r13 )
ARM 에서는 Stack을 위한 명령어가 따로 없습니다. 즉, push나 pop 등의 명령어가 제공되지 않습니다. 그러나 sp라는 키워드를 사용하여 r13을 쓸 수 있는데, 묵시적으로 r13을 스택포인터로 사용할 수 있도록 정해 놓은 듯 합니다. 그리고, push, pop 같은 명령어가 없으므로, ARM에서는 같은 기능을 일반 데이터 전송 명령을 통해 해결합니다. ARM 의 데이터 전송명령은 Auto Increment 기능이 있어서 하나의 인스트럭션으로 Push나 Pop과 동일한 기능을 수행 할 수 있습니다.
( 라고 제가 읽는 문서에서는 설명을 하였지만.. 실제로 현재 최신 ARM CPU는 지원을 합니다. 
   http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0204ik/Babefbce.html 
   위 링크를 따라 가보시면.. ;;  )

 - Link Register ( r14 )
 r14는 링크 레지스터라고 부릅니다. 이 레지스터는 Intel CPU에서 보지 못했던 기능의 레지스터인데요. Intel CPU등의 CPU에서는 서브루틴을 호출할 경우 CALL을 사용하면 다음에 수행될 PC를 스택에 넣고, 호출될 번지를 PC에 넣는 동작을 하게됩니다. 하지만 ARM에서는 CALL과 RET와  같은 명령이 없습니다. 대신 Branch With Link라는 명령(BL)이 있는데, 해당 명령을 수행하면, CALL과 비슷하게 다음에 수행될 pc(r15) 값을 스택이 아니라 lr(r14)에 넣고 분기 번지를 pc(r15)에 넣어 분기합니다. 즉, 스택을 사용하지 않는 것이지요. 복귀할 때는 RET 대신 mov pc,lr 이라는 데이터 전송명령으로 복귀합니다.

이러한 방식은 나름대로 장단점이 있습니다.
단점
어떤 서브루틴이 콜 되었을 때, 서브루틴에서 복귀번지가 r14에 들어있는 상태가 됩니다. 문제는 해당 서브루틴에서 다시 한번 다른 서브루틴을 콜 한다면, 원래 r14에 보관되어 있던 복귀 번지 값이 덮어씌워지는 결과가 생깁니다. 이런 경우엔, 수동으로 sp(r13)를 이용하여 스택에 r14 값을 보관해 두어야 합니다. 즉, 분기(BL) 하기전에 r14를 스택에 보관해 두고, 리턴해서 복구하는 과정을 거치는 셈

그러면 왜 굳이 이러한 방법을 쓸까요?? 여러번 분기(BL)하는 경우가 아닌 한번만 분기(BL) 하는 경우라면, 스택을 사용하지 않고 레지스터를 사용함으로써, 그 속도에서 이익을 얻게 되는 것! 

 또한 분석가 입장에서 보면 CALL, RET 명령어가 없어서 가독성 면에서 떨어질 것 같습니다. 이건 저도 아직 경험해 보지 않은 것이지만 이렇게 이론적으로 공부하더라도 그럴 것 같네요.

2. ARM Status Register  
 위에서 Status 레지스터는 32Bit 짜리 레지스터 6개가 있다고 했습니다. 그러나 6개 모두를 한꺼번에 사용하지는 못하고, 또 그럴 필요도 없습니다. 일단 하나의 32Bit Status 레지스터만 생각하면 됩니다.

  Status 레지스터는 PSR이라고 부릅니다. 그리고 일반적으로 CPSR이라고 하여 Current Program Status Register라고 부릅니다. PSR은 크게 Flag Bits부분과 Control Bits 부분으로 나뉩니다.

 - Flag Bits
 어떤 인스트럭션의 결과 등을 나타내는 부분으로 4Bit가 있습니다. 다른 CPU의 Flag Register와 비슷한데 각각 N, Z, C ,V의 4가지 입니다. 

 1) Negative/Less Than Flag
   N으로 표기되는 이 플래그는 연산의 결과가 마이너스인 경우에 세트됩니다. 
 2) Zero Flag
   Z로 표기되는 이 플래그는 연산의 결과가 0이 되었을 경우에 세트됩니다.
 3) Carry/Borrow/Extend Flag
   C로 표기되는 이 플래그는 자리올림이나 내림이 발생한 경우, 그리고 Shift 연산 등에서 사용됩니다.
 4) Overflow Flag
   V로 표기되는 이 플래그는 연산의 결과가 오버플로우 되었을 경우 사용됩니다.


이상의 Flag Bit들은 다른 칩의 상태 레지스터와 다르지 않습니다.


 - Control Bits
 컨트롤 비트들은 인터럽트를 제어하는 비트와 계속해서 언급되기만하고 실체를 드러내지 않고 있는 Exception과 관련된 CPU 동작모드를 설정하거나 확인할 수 있는 기능을 가진 Bit가 있습니다.

 1) IRQ/FIQ Disable Bit
  ARM 인터럽트 중에서 IRQ와 FIQ를 금지시킬 수 있는 클래그입니다. 인터럽트의 종류는 이밖에도 몇 가지가 더 있는데, 그 중에서 IRQ, FIQ는 PSR를 통해 금지시키거나 가능하도록 설정할 수 있습니다.
 2) Mode Bits
  M0에서 M4까지의 모드 비트는 CPU의 6개의 동작 상태를 나타냅니다. 즉, 간단히 말하면 ARM은 6개의 동작 모드를 가지는데, 이를테면 User모드와 인터럽트 모드 등입니다.


이제 상태 레지스터를 그림으로 봐보겠습니다.



지금까지 레지스터를 알아보았는데 Exception 관련된 부분은 필요할 때 추후 올리겠습니다.

이번엔 Instruction Set을 알아보도록 하죠. 

ARM 명령어 특징
 ARM은 32Bit 코어입니다. 특징적인 것은 모든 명령어가 32Bit 하나의 Word로 구성된다는 것입니다. Intel CPU 경우엔 명령어에 따라 대략 1Byte~7Byte까지 있습니다. 하지만 ARM은 모든 명령어를 한 Word로 처리합니다. 일단은 명령어의 개수가 몇 안되고, 주소는 상대주소 방식을 사용하며, 심지어 Immediate 상수 값도 32Bit 값을 그대로 넣을 수 없습니다.
 만약 r0에 32Bit 상수를 넣고 싶다면, 몇몇 예외를 제외하고는 메모리에 미리 넣어두고 해당 메모리를 상태 주소로 참조해서 얻어와야 한다는 뜻입니다.

 역시 장단점이 있습니다.


장점
 모든 명령어를 같은 사이즈로 처리함에 따라 파이프라인 구현이 용이하다는 점이 있습니다. 그리고 명령어 해석기를 설계할 경우 예외 처리부분이 없으므로, 쉽고 고속으로 처리할 수 있겠지요.

단점
 코딩 시에 몇몇 제한이 따른다는 점 입니다. 상대 주소 지정 방식은, 이 때 사용하는 Offset이 24+2=26Bit 이므로, 상대주소라고는 하지만 거의 불편이 없고, 다만 Immediate 오퍼랜드를 지정할 경우에 좀 번거롭다는 점이 있습니다. 그러나 8Bit 해상도를 가지는 오퍼랜드라면 한 Word 내에서 처리가능 합니다. 

  ARM 명령어의 다른 특징으로는 모든 명령어를 조건적으로 실행시킬 수 있다는 것입니다. 예를 들어, Intel CPU에서는 jz, jc와 같은 점프 명령어를 사용합니다. 그 의미는 jz 같은 경우 zf가 설정되어 있으면 점프를 해라, 혹은 jc같은 경우는 cf가 설정되어 있으면 점프를 해라. 라는 의미입니다. 무조건 점프시에는 jmp를 쓰지요.

 ARM의 경우엔 그런 플래그의 사용이 점프 명령에 국한되지 않고, 예외 없이 모든 명령어에 사용할 수 있습니다.

위의 경우를 보시면 jmp에 해당하는 B명령 뿐만 아니라 MOV 와 같은 데이터 전송명령에도 플래그 옵션을 사용했음을 볼 수 있습니다.

 삼항 연산자로 예를 들어보겠습니다.
a = (b==c) ? d:e;
의미는 b와 c가 같으면 d를 a에 넣고 다르면 e를 a에 넣는 것입니다.

일단 Intel CPU에서는??


이와 같이 됩니다. ARM CPU를 봐보도록 하겠습니다.
제 ipad2 에서 gcc로 컴파일 할 경우 최적화 문제 때문에 원하는 표현이 잘 안나오는 것 같지만
ARM CPU의 명령어를 사용하면

CMPS r2,r3
MOVEQ r1,r4
MOVNE r1,r5

이렇게 짧게 표현 가능합니다. 중요한 것은 Intel CPU에서는 점프문은 꼭 필요한 반면 ARM CPU에서는 점프문이 꼭 필요하진 않습니다.

 다시 한번 강조하지만 ARM에서는 이와 같은 조건 옵션을 모든 명령어에 사용 가능합니다. 실제로 모든 OP 코드의 상위 4Bit는 이런 조건 옵션을 나타내는데 사용되는 비트입니다. 각 조건에 사용되는 접미사 목록입니다.

총 15개로 15 = 1111b 딱 4Bit 맞군요. 다 외울 필요는 없고 여기에서는 그냥 이런 것들이 있구나 하는 정도면 알면 됩니다.
필요할 때 보면되기 때문이죠. 명령어를 볼 땐 명령어에서 다 문자로 표시줍니다.

다음 특징은 대부분의 명령어에 S라는 접미사를 사용하여 플래그 레지스터에 영향을 줄지 여부를 결정할 수 있다는 것입니다. 특히 연산명령을 수행할 때 'S' 를 붙히면 해당 결과에 따라서 플래그 값들이 변하게 되고, 붙히지 않으면 영향을 미치지 않도록 할 수 있습니다. Intel CPU에서는 연산의 결과에 따라 항상 플래그 값이 영향을 받는거와는 다르군요. 사실 이러한 점 때문에 Intel CPU에서는 특정 연산을 수행시 Push , Pop을 사용해 레지스터를 스택에 저장해 두고 연산이 끝난 후 다시 빼오는 방법을 사용하는데.. 귀찮습니다. 

 ● Branch and Branch with link (B, BL)


Cond 부분은 위에 있는 조건 옵션입니다. 101은 B 명령 코드입니다.
L 부분은 1일 경우 BL이 되는 것이고 0이면 B 명령입니다. B는 JMP라고 생각하시면되고, BL은 CALL로 생각하시면 됩니다.
다만 BL의 경우엔 PC값을 스택에 넣는 것이 아니라 r14(lr)에 넣는다는 것에 차이가 있습니다. 나머지 하위 24Bit가 Offset으로 사용되는데 ARM은 모든 명령어들이 Word 단위 이므로 총 +/- 32MB 영역을 커버합니다. 

 ● Data Processing Instruction
데이터 프로세싱 명령은 ARM의 50% 정도에 해당하는 명령입니다. 실제 개수는 16개이고, 연산명령, 비교명령, 비트 연산 명령, 데이터 전송 명령 등이 포함됩니다.


 1) <Cond>
  해당 명령의 조건 실행 플래그입니다. 데이터 프로세싱 명령어에도 당연히 포함됩니다.
 해당 플래그를 통해 명령을 현재 플래그 레지스터(CPSR)의 상태에 따라 실행 여부를 결정하는데 사용되는 플래그입니다. 
 
 2) <I>
  Operland 2로 지정되어 있는 부분이 Immediate Operand 인지 아닌지 여부를 나타내는 비트. Immediate Operand라 함은, 예를 들어 Intel CPU에서 MOV AX, 01234h 라고 했을 경우 1234h를 가리키는 말입니다.

 3) <OpCode>
  데이터 프로세싱 명령 중 어떤 명령인지를 나타내는 필드. 해당 필드와 명령어는 다음과 같습니다.


 4) <S>
  S 비트가 1인 경우는 데이터 프로세싱 명령의 결과가 CPSR에 영향을 미칩니다. 즉, 0인 경우에는 CPSR은 변하지 않습니다.

 5) <Rn>
  ARM 데이터 프로세싱 명령은 그 결과와 첫 번째 오퍼랜드는 항상 레지스터로 지정해야 합니다. Rn은 첫 번째 오퍼랜드를 가리키는 것으로 위에서 Op1으로 표기한 것에 해당합니다. ARM에서 한번에 볼 수 있는 범용 레지스터는 sp, lr, pc 등을 포함해서 r0~r15까지입니다. 즉, 4Bit를 통해 레지스터를 나타내게 됩니다. 해당 필드는 명령에 따라 사용되지 않기도 합니다. MOV나 MVN등이 이에 해당합니다.

 6) <Rd>
  오퍼레이션의 결과가 저장될 레지스터를 의미합니다. 역시 레지스터를 가리키므로 4Bit를 사용하고 모든 명령에서 디폴트로 사용되는 필드. ARM의 데이터 프로세싱 명령의 결과는 항상 레지스터로 들어갑니다.
  
 7) <Operand 2>
Immediate Operand 혹은 레지스터 Operand 입니다. <I> 필드가 0일 경우 레지스터 입니다. 이에 관해선 많은 설명이 있지만 일단 여기까지만... 필요하면 추후..;


이제 조금 더 명령어의 세부 비트 개념이 아닌 관점을 명령어를 읽는 것에 초점을 맞춰보겠습니다.

 1. MOV{cond}{S} Rd, Op2
  Assembler에서 가장 기본적으로 사용되는 데이터 전송명령입니다. ARM에서의 MOV는 다른 칩의 MOV와 비슷하지만 그 Target이 되는 부분이 항상 레지스터라는 점 입니다.
 즉, Intel CPU같은 경우 아래 두 명령어 처리가 가능하지만
MOV EAX,[EBX] 
MOV EAX,EBX 
 
ARM같은 경우는 첫 번째는 처리할 수 없는 명령어 입니다. 즉, 레지스터나 Immediate 값 같은 것들을 레지스터에 넣는 명령입니다. 참고로.. Target 레지스터가 PC인 경우 S옵션을 사용하면, Exception 모드에서 보통 상태로 빠져 나오는 역할을 하게 됩니다. 예시 코드를 보겠습니다.

MOV r0,r1
단순히 r1에 있는 내용을 r0에 넣는 것입니다.

MOV r0,#0
r0에 상수 0을 넣는 명령입니다. ARM에서는 상수에 #을 붙혀서 사용합니다. 또 진법 표현은 C에서의 방법을 사용합니다. 즉, MOV r0,#0x30 이런 식으로 사용할 수도 있습니다. 혹은 &을 붙여서 16진수를 나타낼 수도 있습니다. 

MOV r0, #0xfc000003
r0에 상수 값 0xfc000003을 넣는 명령입니다. 해당 값은 8Bit 값 0xFF를 32Bit로 확장하고 오른쪽으로 두 번 Rotate 시킨 값입니다. 그래서 에러가 나지 않습니다.

MOV r0, r1, LSL #1 
r1을 왼쪽으로 1bit Shift한 값을 r0에 넣는 것입니다. LSL( Logical Shift Left ) 

MOV r0, r1, LSR r2
r1을 r2 만큼 오른쪽으로 Shift 한 값을 r0에 넣는 명령입니다. LSR ( Logical Shift Right )

MOV r0, r0, ASR #24
r0을 오른쪽으로 24bit 만큼 Shift 한 값을 r0에 넣는다. ASR ( Arithmetic Shift Right )
LSR와 차이점은 최 상위 비트가 1인 경우 새로 계속 해당 비트 값을 유지시킨다는 것입니다. 
LSR 같은 경우는 새로 들어오는 최상위 비트는 항상 0입니다.

MOVS r0, r1, LSR #1          ; C(flag) = r1[0]
MOVCC r0, #10                 ; if C == 0 then r0 = 10 
MOVCS r0, #11                 ; if C == 1 then r0 = 11
r1을 오른쪽으로 1bit Shift 한 값을 r0에 넣는다. 이 때 연산의 결과가 플래그에 영향을 미치도록 하기 위해 S를 붙힌 것이다.
만약 Carry Flag가 셋팅 됬을 때와 안됬을 때의 차이로 r0에 넣는 값이 달라진다.

MOVS r0, r4                    ; if r4 == 0 then r0 = 0
MOVNE r0, #1                 ; else r0 = 1
r0에 처음에 무조건 r4 값이 들어가고 MOVNE에서 r4 값이 0이면 0과 1은 다르므로 false로 MOVNE문은 실행되지 않습니다.

2. MVN{cond}{s}   Rd, Op2 
 이 명령어는 Rd = NOT Operand2 의 의미를 가진 명령입니다. 기존 어셈블러에서는 볼 수 없던 명령이죠. 기능은 MOV 처럼 값을 넣긴 넣는데 NOT을 해서 넣는 명령입니다.

MVN r0, #0                   ; r0 = -1
 0을 NOT 하면 FF..FFF 이므로 -1 이 되게 됩니다.
또한 MOV r0, #0xFFFFFFFF 이렇게 하면 에러가 발생합니다. ARM의 모든 명령이 32bit 한 word이고 Immediate 어포랜드처리를 8bit 값을 Rotate 시키는 방식으로 사용하기 때문인데, MVN을 사용함으로써 해당 값을 넣을 수 있습니다. 

3. ADD{cond}{S}  Rd, Rn, Op2
   ADC{cond}{S}  Rd, Rn, Op2

더하기 명령입니다. Intel CPU와 차이점은 오퍼랜드를 3개 지정한다는 것입니다. Rd 는 결과가 저장될 레지스터로, Data Processing 명령 모두는 그 결과가 Rd로 들어가야만 합니다. 나머지 두개가 서로 더해질 오퍼랜드 입니다. 두개의 오퍼랜드 중 하나는 또 항상 레지스터여야만 합니다. Op2는 Shifted Register 혹은 Immediate Value 중 하나입니다. 

ADD r0, r0, #1             ; r0 = r0 + 1
r0에 1을 더한 후 그 값을 r0 넣는 것입니다.

ADD r0, r1, r2             ; r0 = r1 + r2
생략..

ADDS r0, r1, r1, LSL #2     ; r0 = r1 * 5
알아서 유추... 모든 수의 <<1 은.. x2..

ADD r0, r0, r1                 ; r0 = r0 + r1
MOV pc, lr                    ; return
r0 과 r1을 더해 r0에 넣고 리턴 하고 있습니다. 리턴 값은 r0 레지스터에 들어가게 됩니다.

4. SUB{cond}{S}     Rd, Rn, Op2
    SBC{cond}{S}     Rd, Rn, Op2              ; Carry Flag와 관련되어 있는 명령
    RSB{cond}{S}     Rd, Rn, Op2  
    RSC{cond}{S}     Rd, Rn, Op2              ; Carry Flag와 관련되어 있는 명령 


위 네 명령은 빼기 명령입니다. R로 시작하는 명령은 Rn과 Op2를 바꿔서 명령하는 명령입니다. 즉,
SUB Rd, Rn, Op2   => Rd = Rn - Op2
RSB Rd, Rn, Op2   => Rd = Op2 - Rn 

SBC Rd, Rn, Op2   => Rd =  Rn - Op2 + Carry - 1

5. AND{cond}{S}  Rd, Rn, Op2
    EOR{cond}{S}  Rd, Rn, Op2
    ORR{cond}{S}  Rd, Rn, Op2


이번엔 Bit 연산에 관련된 명령어들입니다.
AND => AND
EOR => XOR
ORR => OR 

B 명령을 제외하고는 왠만한 명령어는 모두 3글자라서 ORR로 했나봅니다.

AND r0, r0, #0xFF
r0과 0xFF를 and 연산 후 그 결과를 r0에 넣습니다. r0의 8bit만 남기는 명령입니다.

ANDCSS r0, r1, ASR r3 
만약 캐리플래그가 설정되어 있다면 r0 = r0 AND ( r1 >> r3 ) 을 수행합니다. 계산 결과는 플래그에 영향을 미칩니다.

EORS r0, r0, r0
 r0 = r0 XOR r0 을 수행하는 명령인데 r0을 0으로 만들고 NF는 0, ZF는 1로 만듭니다.

MOV r0, #0xFF
ORR r0, r0, #0xFF00

최종적으로 r0을 0xFFFF를 만드는 과정입니다.

6. BIC{cond}{S}    Rd, Rn, Op2
 Rd = Rn AND (NOT Op2) 로 설명이 되어 있는 명링입니다.

BIC r0, r1, #3
r0 = r1 AND 0xFFFFFFFC 의미입니다.

7. CMP{cond}  Rn, Op2
   CMN{cond}  Rn, Op2


비교 명령입니다. MOV, MVN처럼 인수가 2개이지만 Rd가 없습니다.
CMP는 우리가 알던 CMP명령입니다. Rn에서 Op2를 빼 보는 명령이죠. 해당 명령은 레지스터에는 영향을 미치지 않고 플래그에만 영향을 미치는데 이 명령어들은 S 옵션이 없어도 디폴트로 S 옵션을 준 효과가 나타납니다. S 옵션마저 꺼지면.. -_-
Nop 같은 명령어가 되는건가요?? ㅎ

CMN은 CMP와 반대로 두 오퍼랜드를 더해보는 명령입니다.

CMP r2, #23
MOVEQ r2, #45
 만약 r2가 23이면 45를 넣어라..

CMP r0, #0
CMPEQ r1, #0
CMPEQ r2, #0
CMPEQ r3, #0
MOVEQ r4, #12

r0부터 r3까지 모두 0이라면 r4에 12를 넣는 명령 

CMN r1, r2
MOVEQ r0, #0
MVNNE r0, #1

r0 = (r1 + r2) == 0 ? 0, -1  이라는 의미.. 영어 해석 같네요.. -_-;

8. TEQ{cond}  Rn, Op2
   TST{cond}  Rn, Op2


CMP와 거의 비슷한 구조를 갖는 명령입니다. 기능도 거의 유사..
CMP가 빼기, CMN가 더하기라면
TEQ는 XOR, TST는 AND 연산을 통해 같은 일을 합니다.
하지만 특징이라면 Logical 연산이 일어날 경우 플래그 중에서 VF(Overflow)는 영향을 받지 않습니다.

CMP r0, #31           ; r0 <= 31
TEQ r0, #127          ; r0 == 127
MOVLS r0,#'.'        ; if either then r0 ='.'

TST r1, #3             
MOVEQ r0, #1        
MOVNE r0, #0 

r0 = (r1 & 3) == 0 ? 1 : 0 이러한 의미 같습니다.

 ● PSR Transfer 
PSR이란 Program Status Register 로서 플래그 비트와 Control 비트들로 구성된 레지스터 입니다. 해당 레지스터의 값을 일반 레지스터로 옮기거나 반대의 일을 하는 명령이 PSR Transfer 명령입니다. 

 PSR은 32Bit 레지스터입니다. CPSR, SPSR 5개를 합쳐서 총 6개가 있습니다. 여기서 SPSR은 Exception 모드에 따라 여분으로 존재하는 PSR을 말합니다. 실행 모드에 따라서 User가 접근할 수 있는 PSR은 한개, 혹은 2개로 제한됩니다. 예를 들어 User모드에서는 CPSR에만 접근 할 수 있고, IRQ모드에서는 CPSR과 SPSR_irp 에 접근할 수 있습니다.

아래 명령어는 그닦 쓸일이 없고 CPU 동작 모드를 임의로 설정하거나, 현재 동작모드를 확인하기 위해서 사용되는 경우가 있습니다. 

1.  MRS{cond}  Rd,<psr>     : Transfer PSR contents to a register
    MSR{cond}  <psr>, Rm    : Transfer register contents to PSR
    MSR{cond}  <psrf>, Rm   : Transfer register contents to PSR flag bits only


MRS명령과 MSR 명령의 의미가 애매할 수도 있습니다. M을 Move로, R을 레지스터로, S를 PSF로 파악하면 MRS의 경우엔 Move Reg PSR 정도로 생각할 수 있습니다. 즉, 레지스터에 PSR 값을 넣는 명령

예제 생략.. = _ =

 ● 곱하기 명령(MUL, MLA)
ARM에서는 곱하기 명령이 크게 두 종류가 있습니다. 하나는 그냥 곱하는 것이고, 다른 하나는 곱해서 더하는 것입니다.

 1. MUL{cond}{S}   Rd, Rm, Rs 
이 명령에서는 레지스터만을 사용해야 합니다.

MUL r1, r2, r3              ; r1 = r2 * r3
참고로 곱하기의 결과가 32Bit를 넘는다면, 하위 32Bit만 결과 레지스터에 남습니다.

 2. MLA{cond}{S}   Rd, Rm, Rs, Rn
이번엔 레지스터가 4개 입니다.

Rd = Rm * Rs + Rn 의 의미 입니다. 

 ● Single Data Transfer( LDR, STR )
해당 명령은 레지스터와 외부 메모리와의 데이터 전송을 담당하는 명령입니다. 무척 사용 빈도가 높은 명령. Intel CPU에서는 MOV 하나로 레지스터간의 데이터 전송과, 외부메모리와의 데이터 전송의 두 가지 목적으로 사용하지만 ARM에서는 구분이 되어 있습니다.

 즉, MOV 명령은 레지스터간의 전송명령, 데이터 처리명령으로 분류되고
LDR, STR은 데이터 전송명령으로 분류됩니다. 

LDR은 Load라는 의미로 외부 메모리로부터 레지스터로 데이터를 읽어오는 명령이고,
STR은 Storage라는 의미로 레지스터로부터 외부 메모리로 데이터를 저장하라는 명령입니다.

1. LDR{cond}{B}  Rd, address{!}       ; Rd = Contents of address
   LDR{cond}{B}  Rd, =expression      ; Rd = expression
   STR{cond}{B}  Rd, address{!}       ; contents of address = Rd


LDR과 STR은 Single Data 전송 명령입니다. 비슷한 명령으로 LDM, STM은 여러 개의 레지스터 내용을 전송할 수 있는 명령입니다. 이 밖에 SWP라는 스왑 명령이 있는데, 해당 5개의 명령만이 외부 메모리와 레지스터간의 전송을 가능하게 하는 명령입니다.

 LDR과 STR의 경우 전송 단위를 바이트, 혹은 워드(32bit)단위로 수행 할 수 있습니다.
바이트 단위 전송의 경우 해당 레지스터의 어떤 부분이 사용될지의 여부는 해당 프로세서의 Endian에 달려 있습니다.
또, 워드 전송 명령을 사용할 경우, 메모리 주소는 Word align이 되어야 합니다.

Endian 설명
http://ko.wikipedia.org/wiki/%EC%97%94%EB%94%94%EC%96%B8  

아래 그림은 LDR과 STR에서 사용하는 전송 모드를 나타낸 것입니다.


 1) Pre-Indexed Addressing Mode
이 모드에서는 Rn을 베이스 주소로 사용합니다. 여기에 더해서 Offset을 지정하거나, 혹은 지정하지 않을 수 있습니다. Offset을 사용할 경우에는 해당 Offset을 베이스 주소에서 더하도록 하거나 뺄 수 있는데, 위의 그림에서 +,- 기호가 그것을 나타냅니다.

LDR r0, [r1] 
r1을 베이스로 사용하고 Offset은 지정하지 않은 형태입니다. r0에 r1을 번지로 하는 워드를 읽어드립니다.

LDR r0, [r1, #132] 
Offset으로 132를 지정한 형태입니다.

STR r0, [r1, r2]
베이스는 r1, Offset은 r2를 사용한 형태입니다. 즉,
*(r1 + r2) = r0
사람 해깔리게 우 => 좌가 아닌 이 명령어는 좌 => 우   네요..;

LDR r0, [r1, r2, LSL #2]
베이스 r1, Offset은 r2 << 2 의 형태

이번에는 Write-Back 기능입니다. LDR 명령의 경우 옵션으로 Write-Back을 지정할 수 있는데, 지정할 경우 원래 베이스에 Offset을 더한 값을 다시 베이스 레지스터로 넣는 기능을 합니다. 

STR r2, [r0, #-4]!
r2 = *(r0 - 4)
r0 = r0 - 4

이와 같은 의미가 됩니다. Offset을 적용 시켜서 먼저 명령어를 수행 한 후 베이스 주소와 Offset을 더해주는 것이죠. 
아무튼 여기서 Pre-Index Mode이기 때문에 실제 주소를 구할 때 Offset을 먼저 빼준 것입니다. 

 2) Post-Indexed Addressing Mode
Post 인덱스 모드의 경우에는 Pre 인덱스 모드에 비해 Effective Address는 항상 Rn, 즉, 베이스 주소를 나타내는 레지스터의 값입니다. 그리고 한가지 알아두어야할 것은 Post 인덱스 모드의 경우엔 따로 !를 사용하지 않더라도 이폴트로 Write-Back 모드가 사용됩니다. Post인데 Write-Back이 안된다면, Offset을 사용할 이유가 없군요.

LDR r0, [r1], r2 
대괄호의 사용이 Pre 인덱스의 경우와 다릅니다.
r0 에다 r1을 주소로 하여 값을 읽어오고 다시 r1에는 r2를 더해주는 일을 합니다.

 3) Relative Addressing Mode
해당 Addressing 모드는 어셈블러가 적절히 지원하여 변환해 주는 모드라고 생각하면 됩니다. 

LDR r5, ThreeCubed 
ThreeCubed DCD 27 

위와 같은 경우 실제로는
LDR r5, [PC, #constant]
형태의 코드로 번역해 줍니다. 즉, PC를 베이스로 삼아서 코드를 만드는 것이지요. 만약 해당 Symbol이 지정하는 범위가 너무 커서 상수로 지정할 수 없다면 어셈블을 할 때 에러를 내게 됩니다.

LDR r0, =0x12345678
이 경우 어셈블러가 해당 상수 값을 특정 공간에 모아서 삽입 해 주고(이 공간을 Literal Pool 이라 하는군요.) 역시 마찬가지로 PC를 베이스로 해서 명령코드를 만듭니다. 

 ● Block Data Transfer 명령(LDM, STM)
해당 명령은 LDR, STR과 마찬가지로 실제 메모리에 레지스터의 내용을 전달하거나, 전달받을 수 있는 명령입니다.
LDR 명령이 메모리 번지의 내용을 지정된 레지스터로 가져오는 명령이라면 LDM은 가져오긴 하는데, 여러 개의 레지스터의 내용을 한 큐에 가져오는 명령입니다. 가장 많이 사용되는 경우는 스택 연산인 것 같습니다. ARM에는 Push, Pop 명령이 없습니다. 대신 LDR이나 STR을 쓸 수도 있겠고 ,또 LDM, STM을 쓸 수도 있죠. 

push eax
push ebx
push ecx
push edx
와 같은 Intel CPU 명령이 있다면

ARM Single 데이터 전송 명령으론..
STR r0,[sp],#-4 
STR r1,[sp],#-4 
STR r2,[sp],#-4 
STR r3,[sp],#-4  

ARM Multiple 전송 명령으론..
STMEA sp!,{r0,r1,r2,r3}

{ Reg_List }
중괄호 사이에 전송 대상이 되는 레지스터를 넣어주면 됩니다.
ARM 에서 한 시점에 사용할 수 있는 레지스터의 개수는 r0~r15까지 총 16개입니다.

또한 LDM 명령의 니모닉상에 16bit의 공간이 있어서 각 비트가 레지스터 r0~r15와 1:1대응이 된다는 사실입니다.
따라서 { Reg_List }에는 어떠한 레지스터의 조합도 올 수 있습니다.

{r3,r2,r1} 과 {r1,r2,r3}를 했을 경우 메모리에 저장되는 순서는 같습니다. 위 이유 때문..

위 Multiple 전송 명령에서 STMEA라고 적었는데 EA가 동작모드를 지정하는 부분입니다. 이와 같은 키워드가 8가지가 있고, 동작모드는 4가지가 있습니다.

 여기서 동작모드는 여러 개의 레지스터 값을 메모리로(로부터) 전송할 경우 해당 메모리 번지를 증가시키면서 저장할지, 혹은 감소시키면서 저장할지를 지정하는 것과, 증/감을 하는데, 저장하기 전에 증/감을 할지, 아니면 저장하고나서 증/감을 할지를 지정하는 것을 의미합니다.  

STMFD/LDMFD : Full Desending Stack         ; ARM 컴파일러가 사용하는 스택
STMFA/LDMFA : Full Ascending Stack
STMED/LDMED : Empty Descending Stack
STMEA/LDMEA : Empty Ascending Stack

깔끔하게 그림 한장으로 보시겠습니다.


 ● Single Data Swap

1. SWP{cond}{B}   Rd, Rm, [Rn]
레지스터를 3개 지정하도록!. 실제 동작은 Rd = [Rn], [Rn] = Rm이 한번에 일어나는 명령
B는 Operation이 아니라 Byte Operation을 의미합니다.

SWP r0, r1, [r2]         ; r0 = [r2], [r2] = r1
SWPB r2, r3, [r4]       ; r2 = [r4], [r4] = r3     Bit 0~7까지만 영향을 미침 

 ● Software Interrupt

 1. SWI{cond}    <expression>
SWI는 소프트웨어 인터럽트 명령입니다. SWI 명령이 걸리면 동작모드가 변화합니다. Supervisor 상태로 진입을 하게 됩니다. 이러한 특성 때문에 pSOS의 경우는 System Call의 진입방법으로 사용합니다. 

pSOS 설명
http://en.wikipedia.org/wiki/PSOS  

===========================================================================================
으아........... 대략 5시간의 시간을 걸쳐 겨우겨우 하나의 문서를 읽으면서 정리해보았군요.

이제 대충 ARM CPU에 대해 알았고 명령어들을 알아보았으니 실제로 프로그래밍을 해본다음 간단한 명령어들을 해석해보고 그만쓰겠습니다......... 제발!! 저장하기좀 누르고 싶네요 -_-ㅋㅋㅋ

간단히 두 수를 입력받아 합을 구해주는 프로그램 입니다.
main 함수 부분을 봐보겠습니다. 



왼쪽은 실제 환경에서의 gdb로 봐본 결과이고 오른쪽은 Windows에서 IDA로 디스어셈블링을 해본 결과입니다.
차이점을 아시겠나요?? :-) 

왼쪽에선 push, pop 명령어가 쓰인 반면 오른쪽에선 STMFD, LDMFD 명령어들이 쓰였습니다.
뭐 IDA에서는 최신 ARM 명령어를 지원안하나 보군요. 실제로는 push 명령어를 쓰고 어셈블러에서는 STMFD으로 바뀐다고..하는?!


위 스샷에서
GDB 버전은 6.3.50.20050815-cvs
IDA 버전은 6.1.1100421

이었습니다.

일일이 트레이스 해가며 디버깅을 해보려 했으나.. 왜 gdb에서 breakpoint가 안먹히죠..?
그냥 r 누르니까 bp를 다 무시해버리고 그냥 실행되네요.. 할 수 없이 그냥 코드만 보고 분석해봤습니다.

STMFD SP!, {R7, LR}
스택에 LR 레지스터 값 저장. main 함수 내부적으로 또 다른 함수 호출하므로
LR 레지스터에 값이 덮어 씌워지므로 그것을 막기 위함
저장되면
H |....|LR|R7| L
이런식으로 저장 된다. 

ADD R7, SP, #0
스택 포인터를 R7 레지스터에 더하고 있다.;;

SUB SP, SP, #0x14
스택에 공간을 할당하고 있다. 스택은 높은 주소에서 낮은 주소로 자라기 때문에 ..

STR R0, [SP, #0x1c+var_18]  => [SP, #4]
STR R1, [SP, #0x1c+var_1c]  => [SP]

R0, R1 레지스터에 있는 값을 스택포인터에서 +4, +0 위치에 각각 저장한다.

LDR     R3, =(aInputNumber - 0x2278)
ADD     R3, PC, R3      ; "Input number : "
R3에 "Input number : " 문자열 주소를 가져온다.

MOV     R0, R3          ; char *
BL      _printf
문자열 주소를 첫 번째 인자로해서 printf를 호출

ADD     R2, SP, #0x1C+var_10
ADD     R12, SP, #0x1C+var_14
sp에서 +12, +8 위치에 떨어진 곳의 주소를 각각 R2, R12에 넣는다.
인자 값을 받기 위해 주소를 가져오겠지요.

LDR     R3, =(aDD - 0x2290)
ADD     R3, PC, R3      ; "%d %d"
"%d %d" 문자열 있는 주소를 R3로 가져옴

MOV     R0, R3          ; char *
MOV     R1, R2
MOV     R2, R12
BL      _scanf
각 인자 값 넣어주소 scanf 호출.. 뭐 이런식입니다.

이런 부분은 쉬운 부분이므로  
아래는 각자 알아서...

그만 쓸래요! 으악! 


'My Study > Programming&Theory' 카테고리의 다른 글

Calling Convention Process for AMD 64 Bit  (4) 2012.04.02
Windows rand() 랜덤 비율  (2) 2012.02.14
BlockInput Function  (3) 2012.01.20
My BSOD  (2) 2012.01.08
SwapContext를 이용한 프로세스 찾기  (7) 2012.01.04