본문 바로가기

C++

[C++] 인라인 어셈블리 (Inline Assembly)


서론

개발을 하다가, 이런 코드를 하나 마주했습니다.

extern "C" void idle_cpu() {
    asm volatile("hlt");
}

 

해당 코드를 알아보다가 inline assembly에 대해 새롭게 알게 되어 정리하게 되었습니다.

 

이번 글에서는 아래 내용을 알아보겠습니다.

  • 인라인 어셈블리 알아보기
  • 간단한 실습과 함께 objdump로 명령어 확인하기

인라인 어셈블리?

인라인 어셈블리란

→ C/C++ 코드 안에서 직접 어셈블리 코드를 작성하는 것입니다.

 

1. 사용 예제

asm("hlt")
asm volatile("hlt")

 

2. 사용 이유

일반 C 코드로는 하기 어려운 작업들 때문에 인라인 어셈블리를 사용합니다.

  • CPU 레벨 제어
  • 인터럽트 처리
  • 레지스터 직접 조작
  • and so on ..

3. volatile

컴파일러가 최적화 하는 과정에서 코드를 임의로 재배치 혹은 삭제할 수 있습니다.

그런 최적화를 막기 위해 volatile을 사용해 반드시 실행되도록 강제할 수 있습니다.


예제 1) nop

간단한 예제와 함께 inline assembly에 대해 알아보겠습니다.

 

nop은 아무것도 안하는 CPU 명령어 입니다.

#include <stdio.h>

int main() {
    asm volatile("nop");
    asm volatile("nop");
    printf("done\n");
    return 0;
}
$ g++ -O0 nop.cpp -o nop.o
$ ./nop.o 
done

 

실행 파일을 objdump로 확인해보면:

$ objdump -d nop.o

 

해당 명령어를 수행하면 덤프 내용을 확인할 수 있습니다.

 

이 중 main을 보면:

0000000000001149 <main>:
    1149:       f3 0f 1e fa             endbr64
    114d:       55                      push   %rbp
    114e:       48 89 e5                mov    %rsp,%rbp
    1151:       90                      nop    📌
    1152:       90                      nop    📌
    1153:       48 8d 05 aa 0e 00 00    lea    0xeaa(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    115a:       48 89 c7                mov    %rax,%rdi
    115d:       e8 ee fe ff ff          call   1050 <puts@plt>
    1162:       b8 00 00 00 00          mov    $0x0,%eax
    1167:       5d                      pop    %rbp
    1168:       c3                      ret
  • nop 명령어가 2개 찍혀있고, 코드에서 사용한 nop 명령어 개수와 동일합니다.

예제 2) 변수 조작

다음으로는 두 변수를 더해 레지스터에 값을 집어넣는 예제를 확인해보겠습니다.

  • r: register
  • +: read + write
#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;

    asm volatile(
        "add %1, %0"
        : "+r"(a)
        : "r"(b)
    );

    printf("result = %d\n", a);
    return 0;
}
$ g++ -O0 add.cpp -o add.o
$ ./add.o
result = 30

 

objdump로 확인해보면:

$ objdump -d add.o
0000000000001149 <main>:
    1149:       f3 0f 1e fa             endbr64
    114d:       55                      push   %rbp
    114e:       48 89 e5                mov    %rsp,%rbp
    1151:       48 83 ec 10             sub    $0x10,%rsp
    1155:       c7 45 f8 0a 00 00 00    movl   $0xa,-0x8(%rbp)  📌 10 대입 
    115c:       c7 45 fc 14 00 00 00    movl   $0x14,-0x4(%rbp) 📌 20 대입
    1163:       8b 55 fc                mov    -0x4(%rbp),%edx  
    1166:       8b 45 f8                mov    -0x8(%rbp),%eax  
    1169:       01 d0                   add    %edx,%eax        📌 더해서 eax에 저장
    116b:       89 45 f8                mov    %eax,-0x8(%rbp)
    116e:       8b 45 f8                mov    -0x8(%rbp),%eax  
    1171:       89 c6                   mov    %eax,%esi
    1173:       48 8d 05 8a 0e 00 00    lea    0xe8a(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    117a:       48 89 c7                mov    %rax,%rdi
    117d:       b8 00 00 00 00          mov    $0x0,%eax
    1182:       e8 c9 fe ff ff          call   1050 <printf@plt>
    1187:       b8 00 00 00 00          mov    $0x0,%eax
    118c:       c9                      leave
    118d:       c3                      ret
  • 변수에 값을 대입하고,
  • 더하는 명령어를 확인할 수 있습니다.

예제 3) 함수 점프

어셈블리로 함수를 호출할 수 있습니다.

#include <stdio.h>

extern "C" void target() {
    printf("jumped!\n");
}

int main() {
    asm volatile("call target");
    return 0;
}
$ g++ -O0 jump.cpp -o jump.o

 

objdump로 확인해보면:

$ objdump -d jump.o
0000000000001149 <target>:
    1149:       f3 0f 1e fa             endbr64
    114d:       55                      push   %rbp
    114e:       48 89 e5                mov    %rsp,%rbp
    1151:       48 8d 05 ac 0e 00 00    lea    0xeac(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    1158:       48 89 c7                mov    %rax,%rdi
    115b:       e8 f0 fe ff ff          call   1050 <puts@plt>
    1160:       90                      nop
    1161:       5d                      pop    %rbp
    1162:       c3                      ret

0000000000001163 <main>:
    1163:       f3 0f 1e fa             endbr64
    1167:       55                      push   %rbp
    1168:       48 89 e5                mov    %rsp,%rbp
    116b:       e8 d9 ff ff ff          call   1149 <target>  📌
    1170:       b8 00 00 00 00          mov    $0x0,%eax
    1175:       5d                      pop    %rbp
    1176:       c3                      ret
  • call target을 확인할 수 있습니다.

 

+) name mangling

name mangling은 컴파일러가 임의로 변수나 함수의 이름을 변경하는 것을 의미합니다.

 

위의 코드에서 extern "C"를 지우고 컴파일하면, 컴파일 실패합니다.

#include <stdio.h>

void target() {
    printf("jumped!\n");
}

int main() {
    asm volatile("call target");
    return 0;
}
$ g++ -O0 jump.cpp -o jump.o
/usr/bin/ld: /tmp/cc98IxwS.o: in function `main':
jump.cpp:(.text+0x23): undefined reference to `target'
collect2: error: ld returned 1 exit status

 

이는 .c와 .cpp에서의 차이로 컴파일러가 아래 함수를 각각 다음과 같이 변경합니다.

 

C

void target();
→ target

 

C++

void target();
→ _Z6targetv (컴파일 옵션마다 조금씩 다름)

 

그래서 컴파일러가 target을 못 찾아서 링크 에러가 발생합니다.

extern "C"를 붙여서 .c로 컴파일하도록 알려주어야 합니다.


마무리

여기까지 인라인 어셈블리에 대해 알아보았습니다.

 

감사합니다.