Arm Thumb ( blx, bx, Thumb2, IT명령어, if then )
먼저 Thumb의 종류에는, Thumb과 Thumb2가 있다.
Thumb은 2byte 명령어고, Thumb2는 2byte, 4byte로 명령어를 2개로 나눴다.
CPSR 레지스터에 J, T bit가 있는데 이게 01로 set 되면 thumb 모드로 바뀐다.
Thumb은 뭘까?? Arm 명령어를 Thumb 명령어로 바꾸는 것이다. 바꾸는 이유는 더 짧은 실행 시간과 code density 즉, 코드 사이즈 줄이기 때문이다. 이렇게 바꾸는 걸 Interworking이라 부른다. 굳이 긴 명령어가 필요없고 thumb이 지원하는 선에서 동작 가능하다면 변환해서 동작한다.
어떻게 서로 전환할까??
BLX(Branch link) , BX를 이용하여 함수 이름이나 레지스터를 뒤에 적어줘서 바꾼다.
예를 들어보자, 하나 Arm, 하나는 thumb으로 C 코드를 변환한다고 하자,
mov r0 #1
blx thumb_c_test // r0 = 0x11223344
blx arm_c_test // r0 = 0x55555555
r0에 1을 더해주고, blx를 이용하여 분기한다. 근데 왜 arm도 blx를 적었을까??
blx는 arm을 thumb으로 바꿔주는 명령어인데 말이다. 저렇게 적어줘도 컴파일 오류가 나지 않는다. 컴파일러가 자동으로 bl로 바꿔준다. (c_test는 아래 코드에 있다)
조금 더 자세히 어떤 명령어가 사용됐는지 보자
Thumb
바로 예를 들어보자
( + #pragma GCC target ("thumb")을 해주면 컴파일러가 아래 코드를 thumb 명령어로 바꿔준다.)
mov r0 #1
blx thumb_c_test // r0 = 0x11223344
#pragma GCC target ("thumb")
int thumb_C_test(int a)
{
int c;
c = a + 0x11223343;
return c;
}
----------------- elf ----------------
10063a: 6078 str r0, [r7, #4]
int c;
c = a + 0x11223343;
10063c: 687a ldr r2, [r7, #4]
10063e: f243 3343 movw r3, #13123 ; 0x3343
100642: f2c1 1322 movt r3, #4386 ; 0x1122
100646: 4413 add r3, r2
100648: 60fb str r3, [r7, #12]
return c;
10064a: 68fb ldr r3, [r7, #12]
}
10064c: 4618 mov r0, r3
10064e: 3714 adds r7, #20
100650: 46bd mov sp, r7
100652: f85d 7b04 ldr.w r7, [sp], #4
100656: 4770 bx lr
C 코드가 어떻게 변환됐는지 보면 먼저 str을 이용해 r7 + 4의 주소의 값, 1을 r0로 넣는다. 그리고 같은 주소의 값 1을 가져와서 r2에 넣고, 0x11223343을 2개로 나눈다. 그리곤 add를 통해 r2에 저장돼 있는 1과 r3에 저장돼 있는 값을 더하고 r7 + 12에 있는 메모리에 r3값을 저장한다.
그리고 ldr을 이용하여 반환한다.
기계어 코드를 보면 2byte나 4byte로 돼 있는 걸 볼 수 있다. 이게 thumb 명령어다.
thumb 명령어 format은 16bit 중에 레지스터를 지정할 수 있는 bit가 3개밖에 없다. arm에는 레지스터가 16개라 최소 4bit가 필요한데 말이다. 이에 thumb은 r0~r7 레지스터만 사용한다.
이렇게 지정 레지스터를 사용하는 걸 calling convention 이라고 하는데 아래 정리해 놨으니 필요하다면 읽어보길 추천한다.
2022.08.02 - [내가 하는 공부/Arm] - Arm calling convention ( 레지스터 사용, caller, callee, AAPCS )
또 궁금한 게 있다. 명령어 format이 다른데 어떻게 다시 arm format으로 return 할 수 있을까??
이때 사용되는 것이 bx명령어다. bx는 pc값을 업데이트해서 return 주소로 이동하고 ISA를 바꿔준다. 이때 중요한 건 arm에서 thumb으로 반환할 때는 주소의 LSB가 1이고, thumb에서 arm으로 리턴할 때는 LSB가 0이다.
앞서 BLX는 뒤에 함수 이름을 적어주거나 특정 Rm을 적어주는데 차이점은 함수를 적어줄 때는 언제 컴파일되는지 알 때 사용하고, 레지스터를 적어줄 때는 언제 실행되는지 모를 때 적어준다.
위에서의 예는 함수를 적어줬으니 내가 언제 실행되는지 알고 있기 때문이다. 이렇게 말하면 잘 이해가 안 된다 그래서 예를 들어보겠다.
#paragm GCC target('thumb')
int sum2 (int n1, int n2) {
return (n1 + n2);
}
#paragm GCC target('arm')
int sub2 (int n1, int n2) {
return (n1 - n2);
}
int arm_c_test (char op) {
int res;
int (*fp) (int, int);
switch (op) {
case "+" : fp = sum2; break;
case "-" : fp = sub2; break;
}
res = fp(10, 13);
return (res);
}
위 C 코드에서 switch문은 언제 실행될지 모른다. 이럴 때 blx Rm을 사용한다.
switch문의 컴파일 코드를 보자.
100734: ea000007 b 100758 <arm_C_test_fp+0x4c>
case '+': fp = sum2; break;
100744: ea000006 b 100764 <arm_C_test_fp+0x58>
case '-': fp = sub2; break;
thumb으로 지정한 sum2는 arm코드에서 thumb으로 바꾸는 것이니 주소의 LSB가 홀수로 돼 있고 sub2는 arm에서 arm으로 바꾸는 것이니 LSB가 짝수로 돼 있다.
이렇게 언제 실행될지 모를 때 BLX Rm으로 사용하고 thumb으로 ISA를 변경한다면 LSB가 1로 변경된다는 걸 기억하면 된다.
IT (If Then)
- 대부분의 Arm 명령어는 조건을 비교할 수 있는 condition bit를 가지고 있다. 하지만 Thumb은 16bit 밖에 없어서 조건을 비교하기에 bit가 모자라다. 그래서 IT 명령어가 필요하다.
CPSR에 보면 IT bit가 존재한다. 여기서 보고 어떤 명령어를 수행하는지 판단한다. arm은 조건이 머신 코드에 들어있지만 thumb은 CPSR에 들어있다
IT의 사용 문법은 IT {T|E} {T|E} {T|E}의 문법을 따르는데 무슨 말이냐면 T는 then block이라고 하고 E는 else bolck이라 보면 된다. 조금 달리 말하면 T와 E로 실행문의 수를 정한다. 보면 최대 4개까지 밖에 못 쓴다는 걸 알 수 있다.
예를 들어보자, 아래와 같은 코드는 어떻게 thumb으로 바뀔까?
if (r0 == 0)
r1 = r1 + 1;
else
r2 = r2 + 1;
아래를 보면 CMP를 이용해서 비교한다. 이때 ITE를 하는데 왜냐면 then block에서 실행문이 한 줄이고 else에서 실행문 한 줄이기 때문이다. 그리고 EQ인 이유는 if 문에서의 조건이 ==이기 때문이다. 만약 > 같았다면 gt (great then)라고 사용한다.
addeq일 때 한 줄, addne일 때 한 줄 이렇게 적어준다.
아래 실행에서 eq일 때와 ne일 때의 수에 맞춰서 T와 E를 지정해줘야 한다.
cmp r0, #0
ite EQ
addeq r1, #1
addne r2, #1
조금 더 보면 cmp명령어는 CPSR에서 nzcv를 업데이트하고, it 명령어를 실행하면 CPSR에서 8bit에 해당하는 it bit를 업데이트한다.
그리고 addeq, addne를 실행하는데 이 두 명령어는 동일한 머신 코드를 사용한다. 그럼 어떻게 구분하냐면 CPSR에서 it bit를 확인하고 nzcv bit를 확인해서 어떤 명령어를 실행할지 정한다.
어떻게 조건을 확인하냐면, 먼저 몇 개의 동작이 있는지 확인하고 조건을 확인한다. eq인지 ne인지 gt인지 등등을 확인하는 것이다. 조건에 맞게 왼쪽 3 bit를 설정한다.
예를 들어 eq이면 000이 들어간다. 이유는 arm에서 조건을 확인할 때, 4bit를 사용하는데 맞고 아니고는 1bit로 확인이 가능하니까 왼쪽 3bit만으로 이게 같고 아니고를 비교 하는지, 크고 작고를 비교하는지의 카테고리를 정할 수 있다. 이에 eq이면 원래 0000인데 3bit만 가져와서 000으로 세팅한다.
마지막으로 몇 번의 조건이 있는지 본다. 이렇게 구구절절 말로 하면 뭔 소린지 도통 모른다.
예를 들어보자, 아래의 코드에서 어떻게 머신 코드가 되는지 보자.
ITTEE EQ
LDREQ r0, [r1]
ADDEQ r0, #2
LDRNE r0, [r2]
ADDNE r0, #4
eq라서 000이고 t=0, e=1로 세팅된다. ttee니까 0011이고 다 찼다면 마지막은 1로 세팅된다. 이에 it bits는 0000_0111로 볼 수 있다.
그렇다면 궁금증이 생긴다. 만약 그냥 ite eq라면 어떻게 될까?? 아래 표에 따라 0000_1100이 된다.
다시 돌아와서 0000_0111이라고 했을 때, 제일 왼쪽 4bit를 뽑아서 cpu에 넘겨 조건을 확인한다. 4bit를 뽑은 이유는 arm에서도 조건을 비교할 때 4bit를 사용하기 때문이다.
그다음에는 왼쪽으로 1만큼 shift 시키는데 왼쪽 3bit는 고정시키고 shift 시킨다. 위의 코드에서 순서대로 cpu에 넘어가는 조건 bit를 보면
첫 번째, LDREQ = 0000 ( hole bits = 0000_0111 )
두 번째, ADDEQ = 0000 ( hole bits = 0000_1110 )
세 번째, LDRNE = 0001 ( hole bits = 0001_1100 )
네 번째, ADDNE = 0001 ( hole bits = 0001_1000 )
이렇게 된다.
이상으로 thumb과 it 명령어에 대해 마친다.