
서론
개발을 하다가, 이런 코드를 하나 마주했습니다.
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로 컴파일하도록 알려주어야 합니다.
마무리
여기까지 인라인 어셈블리에 대해 알아보았습니다.
감사합니다.
'C++' 카테고리의 다른 글
| [C/C++] CMake로 빌드 관리하기 (0) | 2026.05.10 |
|---|---|
| [C++] gdb로 확인해보는 virtual과 vtable (0) | 2026.03.02 |
| [C++] gdb로 확인하는 스마트 포인터 관찰 (1) | 2026.02.23 |