The Beautiful Future

ARM GCC Inline Assembler Cookbook 본문

arm assembly

ARM GCC Inline Assembler Cookbook

Small Octopus 2019. 3. 10. 16:08
http://www.ethernut.de/en/documents/arm-inline-asm.html

* 인라인 어셈블러를 사용해야하는 이유 
1. be getting closer to the hardware, c에서 접근 불가능한 레지스터 접근등
2. highly optimized code 를 만들기 위해


* No operation 
/* 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

ConstraintUsage in ARM stateUsage in Thumb state
fFloating point registers f0 .. f7Not available
hNot availableRegisters r8..r15
GImmediate floating point constantNot available
HSame a G, but negatedNot available
IImmediate value in data processing instructions
e.g. ORR R0, R0, #operand
Constant in the range 0 .. 255
e.g. SWI operand
JIndexing constants -4095 .. 4095
e.g. LDR R1, [PC, #operand]
Constant in the range -255 .. -1
e.g. SUB R0, R0, #operand
KSame as I, but invertedSame as I, but shifted
LSame as I, but negatedConstant in the range -7 .. 7
e.g. SUB R0, R1, #operand
lSame as rRegisters r0..r7
e.g. PUSH operand
MConstant 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
mAny valid memory address
NNot availableConstant in the range of 0 .. 31
e.g. LSL R0, R1, #operand
ONot availableConstant that is a multiple of 4 in the range of -508 .. 508
e.g. ADD SP, #operand
rGeneral register r0 .. r15
e.g. SUB operand1, operand2, operand3
Not available
wVector floating point registers s0 .. s31Not available
XAny operand


Constraint Modifier
ModifierSpecifies
=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 movldr 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.

RegisterAlt. NameUsage
r0a1First function argument
Integer function result
Scratch register
r1a2Second function argument
Scratch register
r2a3Third function argument
Scratch register
r3a4Fourth function argument
Scratch register
r4v1Register variable
r5v2Register variable
r6v3Register variable
r7v4Register variable
r8v5Register variable
r9v6
rfp
Register variable
Real frame pointer
r10slStack limit
r11fpArgument pointer
r12ipTemporary workspace
r13spStack pointer
r14lrLink register
Workspace
r15pcProgram counter


'arm assembly' 카테고리의 다른 글

명령어 정리  (0) 2019.05.18
cross product 분석  (0) 2019.03.09
NEON Register 구조  (0) 2019.03.08
어셈작성법 구조  (0) 2019.03.06
Comments