컴퓨터를 배웠다면 누구나 한번쯤은 “C언어”라는 것을 접하게 됩니다. C언어를 통해 이미 입출력을 학습하였다면 콘솔창에서 아래와 같은 구문이 익숙하겠죠.
---
printf(“Hello world!”);
---
여기서 몇 가지 의문이 드는 것이 있습니다. printf 함수는 도대체 어떻게 실행되는걸까요? 또 안에 있는 문자열은 어떻게 시스템 상에서 전달받게 되는 걸까요? 마지막으로, 이런 함수는 어디에서 불러오는 것일까요?
이것의 답은 바로 실행파일의 구조와 실행과정에서 알 수 있습니다.
나중에 설명하겠지만, 궁금증을 해결하기 위해 잠깐 프로그램의 실행 과정을 보여드리겠습니다.
(사진1)
정체불명의 16진수 숫자들 사이에 printf 함수를 호출하는 부분과 “Hello world!”라는 문자열이 전달되고 있다는 것을 대강은 알 수 있습니다. 모두 실행 파일 내부에서 일어나고 있는 일들이었습니다.
위와 같은 과정을 거치기 위해서는 C언어 코드가 컴퓨터가 이해할 수 있는 단순한 언어로 변환되어야 합니다. 여기서 C언어와 같이 사용자가 이해하기 쉬운 언어는 고급언어, 기계어와 같이 컴퓨터가 이해하기 쉬운 언어는 저급언어라고 부릅니다. 결론적으로, 컴퓨터가 C언어 코드를 이해하기 위해서는 고급언어를 저급언어로 변환하는 과정이 필수적이라고 할 수 있겠습니다.
이 과정을 컴파일이라고 합니다.
(사진2)
컴파일 과정에서는 사용자가 이해할 수 있는 C언어 코드를 기계어로 변환합니다.그러나 이 기계어는 숫자들의 조합으로 이루어져 있어 이해하기 어렵습니다. 그래서 이것을 이해가 가능하게끔 간단한 기호와 명령으로 표현하게 됩니다.
이 언어를 어셈블리어라고 합니다.
어셈블리어는 기계어 1라인 당 어셈블리 명령어 1라인씩 대응된다는 특징이 있습니다. 곧, 기계어와 어셈블리어는 일대일대응 관계라는 것을 알 수 있죠. 어셈블리어는 컴퓨터의 종류에 따라, 그리고 문법에 따라 그 종류가 다양합니다. 주로 사용하는 CPU인 8086 CPU에서는 Intel, AT&T 문법을 제공합니다. 위의 사진에서는 AT&T 방식으로 어셈블리어를 출력해 주었습니다.가독성과 편리성을 위해 지금부터는 Intel 방식으로 설명하도록 하겠습니다.
그렇다면 어셈블리어 명령어의 종류에는 어떤 것이 있을까요? 어셈블리어는 사용자가 기계어를 이해할 수 있도록 아주 간단하게 바꾼 것이기 때문에 단순 연산이나 호출, 정리를 위한 명령어로 구성되어 있습니다.
대표적인 명령어를 아래에 정리해 보겠습니다.
명령어 (command) |
의미 (syntax) |
예시 (example) |
push |
stack에 전달된 값을 저장, esp가 내려감 |
push eax |
pop |
esp 위치에 있는 값을 특정 레지스터에 저장, esp가 올라감 |
pop edx |
mov |
레지스터나 메모리에 값을 저장 |
mov eax, 0 / mov ebp, esp |
lea |
레지스터나 메모리에 변수의 주소를 저장 |
lea eax, dword ptr ss:[ebp-4] |
inc |
값을 1 증가하여 저장 |
inc eax |
dec |
값을 1 감소하여 저장 |
dec eax |
add |
레지스터나 메모리의 값으로 덧셈을 함 |
add eax, 5 |
sub |
레지스터나 메모리의 값으로 뺄셈을 함 |
sub eax, ebx |
mul |
레지스터나 메모리의 값으로 곱셈을 함 |
mul eax, 4 |
call |
다른 함수를 호출함 |
call 0x401040 |
ret |
호출했던 부분으로 다시 돌아감 |
ret |
cmp |
두개의 레지스터 또는 값을 비교 |
cmp eax, edx / cmp eax, 0 |
jmp |
특정 주소로 프로그램 흐름을 바꿈 |
jmp 0x8048720 |
int |
OS의 인터럽트 영역을 system call |
int 0x80 |
nop |
아무것도 실행하지 않음 |
nop |
결과적으로 C언어에서 함수를 호출하는 과정을 어셈블리어의 여러 가지 명령어를 통해 구현할 수 있습니다. ▨