The Beautiful Future
ARM GCC Inline Assembler Cookbook 본문
/* NOP example */ asm("mov r0,r0");
매우 짧은 딜레이를 만들어낸다.
*General form of inline inline assembler
asm(code : output operand list : input operand list : clobber list);
*The connection between assembly language and C
/* Rotating bits example */ void function() { int y = 1, x = 2; asm("mov %[result], %[value], ror #1" : [result] "=r" (y) : [value] "r" (x)); } |
* Symbolic name
asm("mov %0, %1, ror #1" : "=r" (result) : "r" (value));
c에서 연결된 레지스터를 순서대로 %0, %1, .... 참조할 수 있다.
* volatile
어셈블리도 c컴파일러가 최적화를 하기때문에 작성한 것 그대로 사용되지 않고
군더더기 어셈블러가 추가되어 컴파일 될 수 있다. 이미 다른 레지스터에 로드
되어잇는데 또 다른 레지스터에 로드해서 사용하는 그리고 또다른 레지스터를
사용해서 저장하는 불필요한 코드가 생성될 수 있다.
또는 작성한 어셈블러에서 일부만 채택될수도 있는데 군더더기 어셈코드로 간주되어
제거되는 케이스. 만약 컴파일러가 똑똑하다면 위의 NOP example은 필요없는 코드로 간주되어
제거 될 것이다.
또는 코드의 순서가 서로 독립적이여서 컴파일러가 순서대로 바꿀수도 있다.
이런 경우들을 피하고 작성한 그대로 동작하기 위해서는 volatile을 명시해야한다.
*clobber
asm volatile("mrs r12, cpsr\n\t"
"orr r12, r12, #0xC0\n\t"
"msr cpsr_c, r12\n\t" ::: "r12", "cc");
c *= b; /* This may fail. */
asm volatile("mrs r12, cpsr\n"
"bic r12, r12, #0xC0\n"
"msr cpsr_c, r12" ::: "r12", "cc");
clobber는 컴파일러에게 변경되는 레지스터를 알려준다. 또한 램메모리도 변경 될 수 있음을 알려준다.
위 코드는 c와 b를 곱하기 전후로 인터럽트를 방지했다 푸는 코드가 들어가 있는데
인터럽트때문에 값이 변경될수도 있기때문에 사용한다고 가정했지만
cc: condion code flag
컴파일러 최적화에 의해서 순서가 바껴버릴 수 있다. 그럼 인터럽트 방지는 아무소용이 없는 것인데
이를 막기위해 clobber가 사용될 수 있다.
"memory" 키워드를 사용하면 램메모리를 변경할수 도 있다는 걸 컴파일러에게 알려주기 때문에
순서가 유지된다. 코드실행 후에 변수들의 값을 예측 못하기때문에 순서 바꾸는 최적화 못한다.
그리고 모든 캐시값을 저장했다 다시 로드해서 사용한다 인터럽트시 ?
asm volatile("mrs r12, cpsr\n\t"
"orr r12, r12, #0xC0\n\t"
"msr cpsr_c, r12\n\t" :: : "r12", "cc", "memory");
c *= b; /* This is safe. */
asm volatile("mrs r12, cpsr\n"
"bic r12, r12, #0xC0\n"
"msr cpsr_c, r12" ::: "r12", "cc", "memory");
위와 같이 메모리키워드를 넣주면 순서가 유지된다. 더 최적화하기위해서는
메모리 키워드를 사용하면 안좋은 점은 모든 캐시값을들 저장 리로드 하기때문에
아래와 같이 더미 변수를 넣어주면 같은 효과를 볼 수있으면서 모든캐시 값을 저장 안해도 되서 더 좋다.
메모리 키워드는 아무래도 캐쉬 전체를 램에 저장하는 듯하다. 오 그렇다! 포인터를 받아 작업한다면
메모리를 캐쉬에 저장하고 쓸테니! 인터럽트시 안전성을 위해서!
asm volatile("mrs r12, cpsr\n\t"
"orr r12, r12, #0xC0\n\t"
"msr cpsr_c, r12\n\t" : "=X" (b) :: "r12", "cc");
c *= b; /* This is safe. */
asm volatile("mrs r12, cpsr\n"
"bic r12, r12, #0xC0\n"
"msr cpsr_c, r12" :: "X" (c) : "r12", "cc");
*Input and output operands
어셈블리 명령은 특정 파입만 받는다. 예를 들면 브랜치 명령어는 점프할 주소를 받는 것 처럼.
하지만 모든 메모리주소가 유효하지는 않는다. 마지막 오피코드가 24비트 오프셋을 받기 때문이다.
브랜치와 교체 명령어는 32비트 목적지 주소를 기대할 것이다.
상수 변수 주소 가 c에서 인라인어셈블러로 올때 왜 그들이 넘어오는지 어떻게쓰일지 알려줘야한다.
Constraint
Constraint | Usage in ARM state | Usage in Thumb state |
f | Floating point registers f0 .. f7 | Not available |
h | Not available | Registers r8..r15 |
G | Immediate floating point constant | Not available |
H | Same a G, but negated | Not available |
I | Immediate value in data processing instructions e.g. ORR R0, R0, #operand | Constant in the range 0 .. 255 e.g. SWI operand |
J | Indexing constants -4095 .. 4095 e.g. LDR R1, [PC, #operand] | Constant in the range -255 .. -1 e.g. SUB R0, R0, #operand |
K | Same as I, but inverted | Same as I, but shifted |
L | Same as I, but negated | Constant in the range -7 .. 7 e.g. SUB R0, R1, #operand |
l | Same as r | Registers r0..r7 e.g. PUSH operand |
M | Constant in the range of 0 .. 32 or a power of 2 e.g. MOV R2, R1, ROR #operand | Constant that is a multiple of 4 in the range of 0 .. 1020 e.g. ADD R0, SP, #operand |
m | Any valid memory address | |
N | Not available | Constant in the range of 0 .. 31 e.g. LSL R0, R1, #operand |
O | Not available | Constant that is a multiple of 4 in the range of -508 .. 508 e.g. ADD SP, #operand |
r | General register r0 .. r15 e.g. SUB operand1, operand2, operand3 | Not available |
w | Vector floating point registers s0 .. s31 | Not available |
X | Any operand |
Constraint Modifier
Modifier | Specifies |
= | Write-only operand, usually used for all output operands |
+ | Read-write operand, must be listed as an output operand |
& | A register that should be used for output only |
아웃풋오퍼랜드는 오직 쓰기만 가능해야한다. c에서 = 대입연산이 가능해야한다.
인풋오퍼랜드는 컴파이러에 의해서 적절한 타입인지 확인하지 않는다.
read & write 같은 레지스터에 할당
asm("mov %[value], %[value], ror #1" : [value] "+r" (y));
+ 모디파이어를 사용해서 읽고 쓰기를 오퍼랜드에 동시에 적용할 수 있다.
그러나 구버전 인라인 어셈은 지원하지 않는다. 구버전에서도 통하는 방법은 아래와 같다.
asm("mov %0, %0, ror #1" : "=r" (value) : "0" (value));
인풋 오퍼랜드의 컨스트레인에 숫자를 쓸수 있고 이는 숫자 번째와 같은 아웃풋과 같은 레지스터를 의미한다.
그런데 컴파일러가 자동으로 인아웃 레지스터를 하나로 쓰는 경우도 있다. 이런경우 이 레지스터가 램에 쓰여지기 전에
다른곳에서 인풋으로 쓰인다면 에러기 때문에 이를 방지해야한다. & 컨스트레인 모디파이어를 추가하여 쓰기 전용으로 만들어
내부적으로 인풋 레지스터로 사용안돼게 해줘야한다. 다른 용도로 쓰이지 않는 레지스터가 된다.
실전 코딩
재사용을 위해 매크로로 만들어 사용하기
#define BYTESWAP(val) \
__asm__ __volatile__ ( \
"eor r3, %1, %1, ror #16\n\t" \
"bic r3, r3, #0x00FF0000\n\t" \
"mov %0, %1, ror #8\n\t" \
"eor %0, %0, r3, lsr #8" \
: "=r" (val) \
: "0"(val) \
: "r3", "cc" \
);
ANSI에서는 asm 대신 __asm__을 volatile 대신 __volatile__을 사용한다.
C 함수로 만들어 사용하기
바이트 스왑 함수 예제
unsigned long ByteSwap(unsigned long val)
{
asm volatile (
"eor r3, %1, %1, ror #16\n\t"
"bic r3, r3, #0x00FF0000\n\t"
"mov %0, %1, ror #8\n\t"
"eor %0, %0, r3, lsr #8"
: "=r" (val)
: "0"(val)
: "r3"
);
return val;
}
Using constants
You can use the mov instruction to load an immediate constant value into a register. Basically, this is limited to values ranging from 0 to 255.
asm("mov r0, %[flag]" : : [flag] "I" (0x80));
But also larger values can be used when rotating the given range by an even number of bits. In other words, any result of
n * 2X
with n is in the mentioned range of 0 to 255 and x is an even number in the range of 0 to 24. Because of rotation, x may be set to 26, 28 or 30, in which case bits 37 to 32 are folded to bits 5 to 0 resp. Last not least, the binary complement of these values may be given, when using mvn instead of mov.
Sometimes you need to jump to a fixed memory address, which may be defined by a preprocessor macro. You can use the following assembly code:
ldr r3, =JMPADDR bx r3
This will work with any legal address value. If the constant fits (for example 0x20000000), then the smart assembler will convert this to
mov r3, #0x20000000 bx r3
If it doesn't fit (for example 0x00F000F0), then the assembler will load the value from the literal pool.
ldr r3, .L1 bx r3 ... .L1: .word 0x00F000F0
With inline assembly it works in the same way. But instead of using ldr, you can simply provide a constant as a register value:
asm volatile("bx %0" : : "r" (JMPADDR));
Depending on the actual value of the constant, either mov, ldr or any of its variants is used. If JMPADDR is defined as 0xFFFFFF00, then the resulting code will be similar to
mvn r3, #0xFF bx r3
The real world is more complicated. It may happen, that we need to load a specific register with a constant. Let's assume, that we want to call a subroutine, but we want to return to another address than the one that follows our branch. This is can be useful when embedded firmware returns from main. In this case we need to load the link register. Here is the assembly code:
ldr lr, =JMPADDR ldr r3, main bx r3
Any idea how to implement this in inline assembly? Here is a solution:
asm volatile( "mov lr, %1\n\t" "bx %0\n\t" : : "r" (main), "I" (JMPADDR));
But there is still a problem. We use mov here and this will work as long as the value of JMPADDR fits. The resulting code will be the same than what we get in pure assembly code. If it doesn't fit, then we need ldr instead. But unfortunately there is no way to express
ldr lr, =JMPADDR
in inline assembly. Instead, we must write
asm volatile( "mov lr, %1\n\t" "bx %0\n\t" : : "r" (main), "r" (JMPADDR));
Compared to the pure assembly code, we end up with an additional statement, using an additional register.
ldr r3, .L1 ldr r2, .L2 mov lr, r2 bx r3
Register Usage
It is always a good idea to analyze the assembly listing output of the C compiler and study the generated code. The following table of the compiler's typical register usage will be probably helpful to understand the code.
Register | Alt. Name | Usage |
r0 | a1 | First function argument Integer function result Scratch register |
r1 | a2 | Second function argument Scratch register |
r2 | a3 | Third function argument Scratch register |
r3 | a4 | Fourth function argument Scratch register |
r4 | v1 | Register variable |
r5 | v2 | Register variable |
r6 | v3 | Register variable |
r7 | v4 | Register variable |
r8 | v5 | Register variable |
r9 | v6 rfp | Register variable Real frame pointer |
r10 | sl | Stack limit |
r11 | fp | Argument pointer |
r12 | ip | Temporary workspace |
r13 | sp | Stack pointer |
r14 | lr | Link register Workspace |
r15 | pc | Program counter |
'arm assembly' 카테고리의 다른 글
명령어 정리 (0) | 2019.05.18 |
---|---|
cross product 분석 (0) | 2019.03.09 |
NEON Register 구조 (0) | 2019.03.08 |
어셈작성법 구조 (0) | 2019.03.06 |