-
[개발도구] - inline assemblyLinux/development tool 2023. 6. 7. 13:16
글의 참고
- https://en.wikipedia.org/wiki/Inline_assembler
- https://en.cppreference.com/w/c/language/asm
- https://wiki.osdev.org/Inline_Assembly
- https://www.ic.unicamp.br/~celio/mc404-s2-2015/docs/ARM-GCC-Inline-Assembler-Cookbook.pdf
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
글의 내용
- 인라인 어셈블리
" inline asembly 는 주로 asm 이라는 키워드를 사용해서 C 코드에서 어셈블리 명령어를 작성할 수 있게 해준다. 참고로, ISO C 기준으로, inline asembly 를 사용하는 standard keyword 는 `asm` 이 아닌, `__asm__` 다. 그러나, 이 글에서 2 개를 혼용해서 사용하고 있으므로, 참고 바람.
" inline assembly 를 컴파일 해주는 친구는 컴파일러다. 그리고, inline assembly 는 assembly 명령어와 동일하다. 단지, __asm__() 안에 작성하는 것뿐이다. 그러므로, assembly 명령어와 마찬가지로 inline asembly 도 컴파일러에서 지원하는 문법을 따라간다. 이 글은 x86 리눅스를 기준으로 하기 때문에 인텔 문법이 아닌, `AT & T` 문법을 따른다(참고로, `AT & T` 문법은 레지스터 이름앞에는`%%` 를 붙여야 한다. 파라미터로 사용되는 변수앞에는 `%` 를 사용한다. 아래 코드를 보면 이해할 수 있을것이다).
int a=10, b; asm ("movl %1, %%eax; movl %%eax, %0;" :"=r"(b) /* output */ :"r"(a) /* input */ :"%eax" /* clobbered register */ );
- Volatile
" GCC는 최적화를 위해 대개 아웃풋이 없거나, `asm goto` 같은 인라인 어셈블리어들을 그냥 없애버린다. 그리고 루프를 도는데, 루프안에서 특정 코드가 계속 동일한 값을 반환한다고 판단되면 해당 코드를 루프밖으로 빼낸다. 아래 `DoCheck` 함수는 `dwSomeValue` 변수를 함수 인자로 받아서, `dwRes` 변수에 아래에서 `assert()`함수가 아니라면, dwRes를 참조하는 코드는 없다.
void DoCheck(uint32_t dwSomeValue) { uint32_t dwRes; // Assumes dwSomeValue is not zero. asm ("bsfl %1,%0" : "=r" (dwRes) : "r" (dwSomeValue) : "cc"); assert(dwRes > 3); }
- 문법 [참고1]
asm ( assembler template : output operands /* optional */ : input operands /* optional */ : list of clobbered registers /* optional */ );
" `베이직 인라인 어셈블리(basic inline assembly)` 에서는 명령어만 작성할 수 있었다. 즉, 아래와 같았다.
asm ( assembler template );
" `확장 인라인 어셈블리`에서는 `오퍼랜드`를 명시할 수 있게 됬다. 즉, 인풋 및 아웃풋 파라미터를 전달할 수 있게 됬다. 오퍼랜드는 필수는 아니다. 그러나, 명령어는 필수다. `확장 인라인 어셈블리`는 3개의 콜론을 기준으로 4개의 구역으로 나눠진다. 첫 번째 구역은 템플릿으로 명령어를 입력하고, 그 다음 구역은 어셈블리어에 보낼 파리미터를 명시, 그 다음은 어셈블리로 부터 받을 파라미터를 입력한다.
" 만약, 아웃풋 파라미터를 보내지 않는데 인풋 값을 받아야할 경우, 아웃풋 오퍼랜드 영역에 아무것도 작성하지 않고 연속해서 콜론(:)이 2개 와야 한다. 아래 예시를 보면 알 수 있다.
asm ("cld\n\t" "rep\n\t" "stosl" : /* no output registers */ : "c" (count), "a" (fill_value), "D" (dest) : "%ecx", "%edi" );
" 아래 코드는 a와 b의 값을 동일하게 만드는 인라인 어셈블리 코드다.
int a=10, b; asm ("movl %1, %%eax; movl %%eax, %0;" :"=r"(b) /* output */ :"r"(a) /* input */ :"%eax" /* clobbered register */ );
1" `b` 는 output operand 를 의미하고 `%0` 에 의해 참조되고 있다. `a` 는 input operand 로 `%1` 에 의해 참조되고 있다. 오퍼랜드의 위치를 보고 잘보고 어디 콜론에 넣을지를 정해야 한다. 인라인 어셈블리에서는 output operand 는 내가 받는 것을 의미한다. input operand 는 인라인 어셈블리로 보내는 것을 의미한다.
2" 인라인 어셈블리에는 오퍼랜드에 많은 제약사항을 걸 수가 있다. 그 중 하나가 `r`이다. `r`은 아무 레지스터든 상관없으니 현재 오퍼랜드를 레지스터에 저장하라는 의미다.
3" 아웃풋 오버랜드에는 `=`가 붙을 수 있다. 이 제약 사항은 이게 아웃풋 오퍼랜드이며 쓰기만 가능한 오퍼랜드라는 것을 GCC에게 알린다
4" `%%` 더블 퍼센트는 레지스터를 의미한다. 이건 GCC에게 오퍼랜드와 레지스터를 구분하라고 알려준다. 오퍼랜드는 PREFIX로 `%` 퍼센트 하나가 붙는다.
5" `clobbered registers`는 세 번째 클론 이후에 나오는 영역이다. 위에 예시로 들면, 이 영역은 GCC에게 `%eax가 asm 명령어 안에서 사용될 예정이니깐 이 레지스터는 건들지마!` 라고 말하는 것과 같다. 그래서 위에 보면 `%%eax`는 명시적으로 사용하는 것을 볼 수 있다.- Constraint
" 인라인 어셈블리는 컴파일러에 의해서 어셈블리 코드로 변환이 되는데, 이 때 어떻게 변환할 지를 지정해 줄 수가 있다. 이 때, 사용하는 키워드가 `Constraint`다.
`m`
" A memory operand is allowed, with any kind of address that the machine supports in general. Note that the letter used for the general memory constraint can be re-defined by a back end using the TARGET_MEM_CONSTRAINT macro.
`r`
" A register operand is allowed provided that it is in a general register.
`0`, `1`, `2`, … `9`
" An operand that matches the specified operand number is allowed. If a digit is used together with letters within the same alternative, the digit should come last.
....
- 참고 : https://gcc.gnu.org/onlinedocs/gcc/Simple-Constraints.html`R`
" Legacy register—the eight integer registers available on all i386 processors (a, b, c, d, si, di, bp, sp).
`q`
" Any register accessible as rl. In 32-bit mode, a, b, c, and d; in 64-bit mode, any integer register.
`a`
" The `a` register
`b`
" The `b` register
`c`
" The `c` register
`D`
" The `di register
....
- 참고 : https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html- Modifier
" `=` 는 output 에만 사용된다. `+` 는 input 과 output 모두에 사용된다. 무슨 말인고 하니, 아래의 코드를 보자.
`=`
" Means that this operand is written to by this instruction: the previous value is discarded and replaced by new data.
`+`
" Means that this operand is both read and written by the instruction. When the compiler fixes up the operands to satisfy the constraints, it needs to know which operands are read by the instruction and which are written by it. ‘=’ identifies an operand which is only written; ‘+’ identifies an operand that is both read and written; all other operands are assumed to only be read If you specify ‘=’ or ‘+’ in a constraint, you put it in the first character of the constraint string.
- 참고 : https://gcc.gnu.org/onlinedocs/gcc/Modifiers.html" x86 에서 시스템 콜을 호출하는 함수를 보여준다. 이 함수에서 `int $0x80`은 시스템 콜을 호출하는 코드다. 거기에 인자로, 4 개가 전달된다. `+` modifier 를 사용해서 ebx, ebc, edx 레지스터들을 시스템 콜의 인자로 전달된다. 이 때, ebx 에는 arg1 매개 변수의 값이 들어간다. 그리고, ecx 에는 arg2 가 들어가며, edx 에는 arg3 가 들어간다. 이 때, 중요한 건 `+` modifier 는 output operand 에서 사용되지만, input 으로도 사용된다는 점에 유의하자. 게다가, `+` modifier 가 선언되면 write 도 가능하다고 했다. 예를 들어, ebx 는 인자로 전달됬지만, 시스템 콜 내부에서 값이 변경될 수 도 있다. 쉽게 `+` modifier 는 call-by-reference 라고 보면 된다.
" `a` 는 eax 를 의미하며, num 값이 저장된다(` :"a" (num) `). 시스템 콜의 결과값은 eax 레지스터에 저장되는데, 이 값을 지역 변수인 `res` 에 저장한다. 결국, 아래 코드에서 전달되는 인자는 총 4개다(eax, ebx, ecx, edx). 거기다가, 스택이 아닌 레지스터를 통해 전달된다는 점에 주의하자.
// https://en.wikipedia.org/wiki/Inline_assembler extern int errno; int syscall3(int num, int arg1, int arg2, int arg3) { int res; __asm__ ( "int $0x80" /* make the request to the OS */ : "=a" (res), /* return result in eax ("a") */ "+b" (arg1), /* pass arg1 in ebx ("b") [as a "+" output because the syscall may change it] */ "+c" (arg2), /* pass arg2 in ecx ("c") [ditto] */ "+d" (arg3) /* pass arg3 in edx ("d") [ditto] */ : "a" (num) /* pass system call number in eax ("a") */ : "memory", "cc", /* announce to the compiler that the memory and condition codes have been modified */ "esi", "edi", "ebp"); /* these registers are clobbered [changed by the syscall] too */ /* The operating system will return a negative value on error; * wrappers return -1 on error and set the errno global variable */ if (-125 <= res && res < 0) { errno = -res; res = -1; } return res; }
- Clobbered Registers List
" 개발자가 `Clobbered Registers List` 에 특정 레지스터를 명시할 경우, 컴파일러는 해당 레지스터를 사용하지 못한다. 컴파일러는 연산 속도를 최적화를 위해 대부분의 경우, 메모리보다는 레지스터를 사용한다. 그런데, 우리가 어셈블리 코딩을 하다가 `ebx`에 중요한 정보를 보존해야 하는 경우가 생겼다. 그런데, 이 때, 인라인 어셈블리를 사용하면 컴파일러는 어셈블리에 대해 아무것도 모르기 때문에, 오직 최적화를 위해서 `ebx`의 내용을 엎어 쓸 수가 있다. 이럴 때, `Clobbered Registers List`에 `ebx`를 명시하면 컴파일러는 `ebx`를 사용하지 않고, 다른 레지스터를 사용하게 된다.
....
Since the compiler uses CPU registers for internal optimization of your C/C++ variables, and doesn't know about ASM opcodes, you have to warn it about any registers that might get clobbered as a side effect, so the compiler can save their contents before making your ASM call.
....
- 참고 : https://wiki.osdev.org/Inline_Assembly#Clobbered_Registers_List
...
When the compiler selects which registers to use to represent input and output operands, it does not use any of the clobbered registers. As a result, clobbered registers are available for any use in the assembler code.
...
- 참고 : https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Volatile" 이 말은 다른 말로하면, `인라인 어셈블리 코드에 사용자가 `ebx` 를 직접 명시해서 사용할테니, 너는 다른 레지스터를 사용해라` 라고 말하는 것과 같다. 즉, 아래의 경우는 `eax`를 직접 사용하겠다는 의미로 `Clobbered Registers List` 에 `eax` 를 명시한것이다.
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax" /* clobbered register */
);" 즉, `Clobbered Registers List` 는 값을 보존하기 위한 레지스터를 명시한다고 보면 된다.
1. cc
" assembler code 가 flag register 를 변경한다는 것을 컴파일러에게 알려준다. 근데, 사실 이건 실수로 빼먹을 소지가 너무 많다. 그리고, 개발자는 자신이 작성한 어셈블리 코드가 flag register 를 변경할 것이라는 것을 알기가 쉽지 않다. 그래서, x86 같은 경우는 그냥 asm() 을 사용하면, `cc` clobber 가 암묵적으로 사용된다고 한다.
2. memory
" `memory clobber`를 사용할 경우, 컴파일러가 `memory re-ordering`을 하지 못하게 막는다. 즉, 성능을 희생하지만, 동기화를 목적으로 수행하는 명령어다.- how to convert inline assembly to assembly
" 인라인 어셈블리 코드가 어떻게 어셈블리 변환됬는지, `objdump`를 통해 확인이 가능하다. `pdes` 의 주소는 0x101000, 아무 레지스터나 사용해서 `pdes`의 주소를 저하라고 했는데, `edx`를 사용한 모양이다.
__asm__("movl %0, %%eax\n\t"
"movl %%eax, %%cr3\n\t"
::"r"(pdes)
:"%eax"
);c0106d5e: 8b 15 00 10 10 00 mov 0x101000,%edx
c0106d64: 89 d0 mov %edx,%eax
c0106d66: 0f 22 d8 mov %eax,%cr3" 왜 코드 길이가 늘어났을까? 코드가 늘어났으니, 속도가 느려질까? 그렇지 않다. 컴파일러는 pdes 의 값을 알고있고, pdes 값을 메모리에서 가져오는 것은 속도가 느리다고 판단하고, pdes 를 0x101000 으로 치환해서 레지스터에 저장한 것이다. 이렇게 하면, 메모리에 접근할 필요없이 모든 과정이 CPU 내부에서 처리 되기 때문에, 퍼포먼스를 최적화 할 수 있다. 파이프라인 최적화를 위해서 추가적인 레지스터가 사용되는 경우도 있지만, 아래 케이스에는 속하지 않는다.
'Linux > development tool' 카테고리의 다른 글
[GIT] - git diff를 통한 patch 파일 생성 및 적용 (0) 2023.08.03 Shell script (0) 2023.07.10 dd (0) 2023.06.10 [개발 도구] objdump (0) 2023.06.10 [개발도구] - Linker (0) 2023.06.04