소프트웨어에 입문하는 분들은 컴파일러란 단어를 매우 자주 들을 가능성이 높습니다. 왜냐면 현업에서 가장 많이 쓰는 단어 중 하나이거든요.
그럼 컴파일이란 단어를 영한 사전으로 찾으면 "안내서를 만들다.", "책을 편집하다."란 의미입니다.
그러면 "컴퓨터에게 편집해서 안내서를 만든다"란 문장으로 컴파일이란 단어를 조합할 수 있는 것 같습니다.
x86, ARM과 같은 CPU가 해석할 수 있는 것은 오로지 명령어이며, 이는 비트 패턴인 기계어를 뜻합니다.
"프로세스는 이미 정해진 특정한 비트 패턴에 반응한다"란 의미로 특정 비트 패턴을 명령어라고 부를 수 있습니다.
조금 쉽게 설명을 드리면 CPU는 여러가지 전기적 스위치로 구성돼 있으며, 어떤 특정한 전기 스위치를 작동시키려면 데이터 버스 선을 따라 전압이 "있고 있고 없고 없고 없고 없고 없고 있고 있고"의 상태로 만들어 주면 된다" 라는 의미와 같습니다.
예를 들어 다음 코드를 함께 볼까요? start_kernel란 함수를 어셈블리 명령어입니다.
어셈블리 코드를 보면 아시겠지만 아래 코드는 ARM 프로세스 CPU에서 돌고 있습니다.
NSR:C1A008D4|E92D43F0 start_kernel: push {r4-r9,r14}
NSR:C1A008D8|E24DD01C sub r13,r13,#0x1C ; r13,r13,#28
NSR:C1A008DC|E59F03A0 ldr r0,0xC1A00C84
C1A008D8 주소를 보면 E24DD01C이란 16진수가 있습니다. 이를 비트 패턴으로 바꾸면 다음과 같죠.
E 2 4 D D 0 1 C
1110|0010|0100|1101|1101|0000|0001|1100
"1110|0010|0100|1101|1101|0000|0001|1100" 란 비트 패턴(16진수: E24DD01C)으로 스택 주소를 0x1C만큼 빼서 스택 주소에 저장하라고 정의됐다고 한다면요.
ARM 프로세서에게 스택 주소를 0x1C만큼 빼서 스택 주소에 저장하라란 일을 시키고 싶으면
"1110|0010|0100|1101|1101|0000|0001|1100" 비트 열을 데이터 버스를 통해 ARM 프로세스에게 전달하면 ARM 프로세스는 이미 정해진 약속에 맞게 일을 합니다. 참고로 이런 비트 패턴을 하드웨어적으로 해석하는 부분은 디코더입니다.
CPU 입장에서 기계어를 살펴봤으니 위에서 다른 내용을 바탕으로 프로그램이란 용어에 대해서 조금 살펴볼까요?
프로그램이란 위에서 다른 바와 같이 CPU가 일을 하게 하는 일련의 기계어 명령의 순차적인 집합입니다.
그런데 예전 컴퓨터가 처음 발명했을 때 이런 비트 패턴을 사람이 직접 CPU에게 입력 했습니다. 지금 2018년에는 이해하기 힘든 고된 노동이였다고 생각되는데요. 공사판에서 모래, 자갈을 구분하듯이 비트 패턴을 일일히 개발자가 입력하는 모습 상상하기 어렵죠?
이렇게 일을 하다 보니 사람이 이진수 나열을 해석하기 어려우니 이런 비트 패턴(기계어) "1110|0010|0100|1101|1101|0000|0001|1100"에 대응하는 어셈블리란 개념을 도입했습니다.
이런 기계어와 1:1로 대응하여 사람들이 알아보기 쉬운 체계이라고 할 수 있죠.
이번에도 어셈블리 코드를 예를 들께요.
NSR:C1A008D4|E92D43F0 start_kernel: push {r4-r9,r14}
NSR:C1A008D8|E24DD01C sub r13,r13,#0x1C ; r13,r13,#28
NSR:C1A008DC|E59F03A0 ldr r0,0xC1A00C84
C1A008D4 주소에 있는 "push {r4-r9,r14}"란 명령어를 비트 패턴으로 분석하면 E92D43F0와 같은 동작이며, 이를 비트 패턴으로 나열하면 "11101001001011010100001111110000"이 되는 것이죠. 이런 어셈블리 코드는 기계어와 바로 대응되어 개발자가 훨씬 편하게 일하게 됐습니다.
그래서 개발자는 "11101001001011010100001111110000"와 같은 비트 패턴을 일일히 입력하지 않고, 어셈블리로 코딩을 하게 됐습니다.
이런 어셈블리어를 기계어로 바꿔주는 일을 컴파일러가 하게 된 것입니다.
"push {r4-r9,r14}" --> 컴파일러 --> "11101001001011010100001111110000" 비트 패턴
그런데 기계어에 매핑하는 어셈블러는 특정 CPU에서만 동작합니다. 예를 들어 위해서 알아본 어셈블리 명령어는 x86에서는 동작하지 않습니다.
CPU는 이미 정해진 특정 비트 패턴을 전달해야 약속된 일을 하기 때문에 CPU마다 다른 어셈블러를 지원합니다.
예를 들어 "push {r4-r9,r14}" 와 같이 ARM CPU에서 적용하는 명령어를 x86에서는 쓸 수 없다는 것이죠. 달리 말하면 호환성이 떨어진다고 할 수 있겠네요.
그래서 서로 다른 CPU(x86, ARM)에 맞게 맞춤으로 어셈블리를 생성하는 컴파일러가 있으면 참 편하겠다는 생각이 들었습니다.
그래서 C나 C++과 같은 언어를 지원하는 컴파일러를 만들었습니다. 그래서 C언어로 코드를 작성한 후 해당 코드가 동작할 CPU에 맞는 C 컴파일러를 이용하는 것이죠. 그러면 해당 CPU에서 동작 가능한 어셈블리를 컴파일러가 생성한다는 것입니다.
예를 들어, ARM CPU의 경우 C 언어로 코드를 작성한 후 컴파일을 한다는 의미는 C 컴파일러가 ARM CPU가 동작할 수 있는 어셈블리를 만든 후, ARM 어셈블러가 이를 ARM CPU가
해석할 수 있는 일련의 비트 패턴을 생성한다고 할 수 있습니다.
이런 비트 패턴을 16진수 포멧으로 떡과 같이 뭉쳐 놓은 것을 흔히 Executable Binary Image라 합니다.
'Core BSP 분석 > 리눅스 커널 핵심 분석' 카테고리의 다른 글
[C언어] 포인터 (p + 1) 연산 (0) | 2023.05.06 |
---|---|
[Linux][GCC]## 매크로 - 심볼 생성 (0) | 2023.05.06 |
GCC - C언어 매크로(Macro) -(1) (0) | 2023.05.06 |
LKML - [PATCH v2] clk: fix reentrancy of clk_enable() on UP systems (0) | 2023.05.06 |
와치독 Watchdog Kick 동작 비교 (Qualcomm vs Intel vs Mediatek vs nVidia) (0) | 2023.05.06 |