프로그래밍/General

내 소스 코드가 실행되기까지의 과정

Lou Park 2022. 1. 27. 23:15

# 컴파일 언어

컴파일(Compile)은 어떤 언어를 다른 언어로 바꾸어주는 과정이다.

int a = 30과 같이 우리가 적는 코드는 사람이 이해하기 쉬운 고급언어(High-level language)라서 컴퓨터는 이를 이해하지 못한다. 

 

컴퓨터는 고급언어로 작성된 코드를 컴파일러 또는 인터프리터를 통해서 저급언어(Low-level language)로 번역한 후에야 코드를 실행할 수 있다. 저급언어에는 기계어 (Machine language)와 어셈블리어 (Assembly language)가 있다.

 

- 기계어

CPU가 명령을 처리할때 사용하는 언어, 2진수로 이루어져있다.

1000 1011 0100 0101 1111 1000
1000 0011 1100 0100 0000 1100

 

- 어셈블리어

기계어의 숫자를 조금더 이해하기 쉽게 만든 언어. 명령어가 기계어와 1:1 대응된다. 어셈블리어로 작성된 코드를 기계어로 바꾸어주는 것이 어셈블러(Assembler)다. 직접 레지스터를 다루고, CPU마다 지원하는 명령어가 다르기때문에 동일한 종류의 프로세서에서만 실행되어 프로세서에 대한 사전 지식이 필요하다.

 push        ebp  
 mov         ebp,esp  
 sub         esp,0E4h  
 push        ebx

 

 

컴파일 언어는 프로그래머가 작성한 코드가 컴파일러를 통해 기계어로 컴파일된 후 실행되는 언어다. 인터프리터 언어와 비교하면 빌드과정에 드는 시간이 추가되지만, 런타임 중에는 이미 기계어로 모든 소스가 변환되어있기 때문에 실행속도가 빠르다는 장점이 있다. 대표적인 컴파일 언어에는 C, C++, Erlang, Haskell, Rust, Go 등이 있다.

 

 

직접 빌드해보기

그럼 간단한 c 파일을 빌드해보자. 

빌드는 전처리 - 컴파일 - 링크가 모두 포함되는 과정이다. 

gcc(GNU compiler collection)로 빌드를 해볼건데, gcc는 unix계열 운영체제에 모두 사용할 수 있는 컴파일러다.

 

 

 

 

gcc가 설치되어있지 않다면 아래 명령어로 설치한다.

sudo apt install build-essential

 

이제 편집기로 간단한 c 프로그램을 작성해 보겠다.

hello.h라는 헤더파일을 생성하여 다음과 같이 hello라는 함수의 프로토타입을 적어준다.

void hello (const char* name);

hello_impl.c 파일을 생성하여 hello.h 헤더 파일을 불러들여 hello() 구현부를 작성한다.

#include <stdio.h>
#include "hello.h"

void hello(const char* name) {
        printf("Hello, %s!\n", name);
}

main.c 파일을 만들고 hello.h를 선언하여 우리가 만든 hello 함수를 사용해본다.

#include "hello.h"

int main(void) {
        hello("Lou");
        return 0;
}

 

1. 전처리(Preprocessing)

전처리는 다음과 같은 일들이 벌어진다. C/C++에서 #include/#define 등등 구문들이 처리가된다.

- 소스파일 상의 주석을 제거한다.

- 헤더 파일의 코드를 포함한다.

- 매크로를 모두 값으로 변환한다.

 

gcc에 -E 옵션을 주게되면 *.i 확장자의 전처리된 직후 코드를 보여주는데...영 알아보지 못하겠으므로 패쓰한다.

 

2. 컴파일(Complie)

gcc에 -S 옵션을 주게되면 다음과 같이 컴파일 된 *.s 확장자 파일들이 생성된다.

열어보면 다음과 같이 c코드가 어셈블리어로 변환된 모습이 보인다.

하지만 어셈블리 코드를 다시 기계어로 번역하는 과정이 또 필요하다. gcc에 -c 옵션을 주게되면 *.o 확장자의 object 파일이 만들어진다.

gcc -c main.c hello_impl.c

오브젝트 파일은 기계어로 적혀있어서 우리가 읽을 수 없다. 

굳이 보고싶다면 hexdump main.o로 열어보면된다. 아래가 그 내용중 일부이다. 아빠 넥타이 확대한거같음~

 

3. 링킹(Linking)

원본 소스 코드에서 c 파일이 여러개니까 object 파일도 여러개 생성되었는데, 이를 하나의 실행가능한 파일로 묶어주는 작업이 필요하다. 이 과정을 링킹이라고 한다.

 

gcc -o 옵션을 통해서 최종적으로 실행가능한 파일을 생성할 수 있다.

gcc main.c hello_impl.c -o program

program을 실행해보면 다음처럼 프로그램이 실행된다.

# 하이브리드 언어

 

이름처럼 컴파일과 인터프리트 기법을 같이 사용하는 언어다. 대표적인 언어로는 Java/C#이 있다. Java는 원시 코드를 바이트 코드(bytecode)로 변환한 뒤에 JVM (Java virtual machine)이라는 프로그램을 통해 바이트 코드를 기계어로 바꾼다. 바이트 코드는 VM에서 처리는 가상 머신을 위한 중간 언어(Intermediate language)다. 컴파일된 바이트 코드는 클래스 파일(*.class)로 생성된다. 바이트 코드는 애플리케이션의 런타임에 JVM에서 인터프리트 방식으로 한줄한줄 읽히며 실행된다.

 

C는 직접 기계어를 생성했기 때문에 운영체제마다 다른 Object 파일을 생성해야하므로 컴파일을 각각 해주어야했지만, Java는 JVM이 읽을 수 있는 바이트 코드를 한번만 컴파일 한 뒤에, 각 JRE(Java runtime environment)가 설치된 운영체제의 JVM 위에서 실행시키기만 하면 되므로 WORA(Write Once Run Anywhere)를 실현시킬 수 있었다.

 

바이트코드가 인터프리트 방식으로 실행되면 런타임 속도가 느려질 수 있는데, 이를 보완하기 위해서 JIT / AOT 컴파일 방식이 고안되었다. 

 

 

APK 파일 분해 

apk 파일을 압축 해제한 모습

안드로이드 앱은 DVM (Dalvik Virtual Machine)이라는 가상 머신 위에서 실행된다. JVM과 비교해서 적은 리소스를 사용하여 열악한 모바일 환경에서도 잘작동하고, 라이센스 관련문제로 구글은 Dalvik을 선택했다고 한다. 아무튼 apk 파일을 압축해제해보면 위 사진처럼 classes.dex라는 파일들이 보일 것이다. 앞서 java파일은 class라는 바이트코드 파일 형식으로 컴파일된다고 했는데, dex는 DVM에서 실행가능한 class 파일의 묶음이다.

 

APK가 만들어지는 과정

Dalvik의 내부 컴파일러가 바로 JIT이다. JIT의 특징을 보면 런타임의 퍼포먼스가 떨어진다는 부분이있는데, 이를 보완하기 위해서 AOT가 등장했다.

 

JIT (Just In Time)

- 앱이 실행되는 순간 자주 사용되는 바이트 코드를 컴파일하여 기계어로 변환 후 메모리에 올려 사용한다.

- 실행중 컴파일이 빈번히 발생하여 메모리 점유율, 배터리 소모 이슈

- AOT 대비 설치 용량이 적다. 

 

Dalvik을 보완하기위해 나온 ART(Android Runtine)는 가상머신이 아니다. Android 5.0부터 지원되는, 기기의 기본 런타임 환경이다. Dex 바이트 코드와 호환가능한데, 앱 설치시 dex2oat 도구를 사용해서 dex파일을 ART에서 실행가능한 oat 파일로 컴파일하기 때문이다. oat 파일은 기계어 형태로 되어있어 실행중에 추가적인 컴파일이 필요하지 않다.

Dalvik vs ART https://brunch.co.kr/@mystoryg/82

 

AOT (Ahead Of Time)

- Android에서는 Kitkat 이후로 도입됨

- 앱을 설치할때 모든 코드를 기계어로 변환(*.oat)하여 ROM에 저장, 그래서 차지 용량도 크고 앱 설치속도가 느림.

- 앱 실행시 컴파일할게 없기때문에 컴파일로 인한 지연이 없어서 실행속도가 개선됨.

 

 

Javascript?

자바스크립트도 하이브리드 언어에 해당한다. 기본적으로 원시 코드를 자바스크립트 엔진이 바이트 코드로 변환하여 가상 머신을 통해 기계어로 번역된다는 구조는 같지만, 엔진 종류에 따라 세부 구현 사항이 다르다.

 

- V8 (Google Chrome, Node.js, Electron)

- Javascript Core (Safari)

- SpiderMonkey(Mozilla Firefox)

- Chakra(MS Edge)

 

# 인터프리트 언어

인터프리트언어는 Object 파일을 생성하지 않고 바로 실행된다. 소스코드의 한 명령 세트마다 인터프리터(Interpreter)가 기계어로 번역해주면서 실행하는 방식이다. 

 

파이썬 인터프리터

 

대표적인 언어로는 Bash, Lua, Perl 등이있다. 인터프리트 언어의 특징이라고 하면 소스 코드 수정 후에 바로 실행시킬 수 있다는 장점이있지만, 한 줄씩 기계어로 번역하기 때문에 런타임 속도가 컴파일 언어보다 느리다는 것이 있다. 

 

스크립트 언어라고도 한다. 다만, 포함되는 관계다. 스크립트 언어는 이미 존재하는 소프트웨어(애플리케이션)을 제어하기 위한 용도로 쓰이는 언어인데, (예를 들면 브라우저를 제어하기 위한 언어인 Javascript) 용도상 수정이 빈번히 발생하여 인터프리트 방식으로 사용하는 것이 좋기때문에 스크립트 언어 대부분이 인터프리트 언어다.

 

 

Python의 인터프리터

Pycharm에서 작성한 프로그램의 Run/Debug Configurations에보면 Python interpreter를 선택하는 부분이 있다. 작성한 Python코드가 어느 버전의 interpreter를 통해 실행 될 것인가를 설정하는 부분이다. 

Python은 대개는 인터프리터 언어라고 한다. 하지만 Python은 엄밀히 말하면 애매하다. Python은 기본 인터프리터로 C로 구현된 CPython을 사용한다. CPython은 C로 작성된 파이썬 구현체로, 파이썬 코드를 가상 머신에 의해 해석되는 바이트 코드로 컴파일한 후 인터프리터로 바이트코드를 한줄한줄 처리한다. 이 외에도 Java 바이트 코드로 만드는 Jython, 그리고 Python으로 구현된 PyPy가 있다.

 

PyPy는 반복을 자주하거나 순수 Python 라이브러리로 구성된 프로젝트에서 퍼포먼스가 좋다. PyPy는 JIT 컴파일러를 탑재하여 자주 쓰이는 코드를 캐싱해두어 인터프리터의 느린 속도를 보완했기 때문이다.

 

이를 직접 체감하기 위해서 컴퓨터에 Pypy를 설치하여 직접 실험해보았다.

 

if __name__ == '__main__':

    start_time = time.time()

    total = 0
    for i in range(1, 10000):
        for j in range(1, 10000):
            total += i + j

    print(f"The result is {total}")

    end_time = time.time()
    print(f"It took {end_time - start_time:.2f} seconds to compute")

엄청난 반복문으로 절대적으로 PyPy에게 유리한 코드기는 하지만... 결과는 PyPy의 압승이다.

# Python 3.10
The result is 999800010000
It took 9.48 seconds to compute

# PyPy 3.8
The result is 999800010000
It took 0.20 seconds to compute

* 글 작성기준 pandas / numpy 도 pypy를 지원한다고 한다.

 

 

# 엄랭으로 느껴보는 구현체~

구현체라는 말이 무엇인지 와닿지 않았는데, 엄랭(엄준식 랭귀지)을 파이썬으로 실행가능하게 구현한 파이썬 구현체를 보니 이해가 갔다. 다른 언어를 실행가능하게 해주는 것이 구현체인 것이다. 엄랭의 파이썬 구현체를 직접 실행시켜보기 위해 엄랭을 잠시 배워보았다.

 

예제코드를 작성해보았는데 쓰다보니 읽을만해지더라...ㅋㅋㅋㅋㅋㅋ

영어가 모국어인 사람들은 코드가 이렇게 보이는 걸까...?

어떻게

엄..
어엄.
식어!
동탄어어?화이팅!
어엄어어,
준.....

이 사람이름이냐ㅋㅋ

위 코드를 엄랭의 파이썬 구현체로 돌리면 대략 다음처럼 작동하게 된다. DEF는 정의, MOVE는 해당 라인으로 점프하는 구문이다.

\n
\n
DEF data[0] = 2
DEF data[1] = 1
PRINT data[0]
IF data[1] == 0:
	END 0
DEF data[1] = 1 - 1
MOVE 5

https://github.com/rycont/umjunsik-lang/blob/master/umjunsik-lang-python/runtime.py

 

GitHub - rycont/umjunsik-lang: 어떻게 엄준식이 언어이름이냐🤣

어떻게 엄준식이 언어이름이냐🤣. Contribute to rycont/umjunsik-lang development by creating an account on GitHub.

github.com

 

# 인터프리트 언어 vs 컴파일 언어

문법상 오류가 있을때 알려주는 시점

다음의 C++ 코드를 살펴보자. 

#include "CMakeProject1.h"

using namespace std;

int main()
{
	cout << "hello" << endl;
	cout << sum(3, 2) << endl;
	return 0;
}

int sum(int a, int b) {
	return a + b;
}

 

이 코드는 빌드되지 못한다. main 함수가 먼저 컴파일되는데, 컴파일되는 시점에서 sum 함수가 무엇인지 알 수 없기 때문이다.

 

다음의 python 코드를 보자. 

if __name__ == '__main__':
    print("hello")
    print(do_sum(3, 2))

def do_sum(a, b):
    return a + b

python코드는 실행된다. 일단 실행하고! Interpreter로 한줄 한줄 읽어 "hello"를 출력한 뒤에 do_sum을 호출하는 시점에서 do_sum이 정의되어있지 않다고 오류를 뱉는다.

 


참고자료

https://st-lab.tistory.com/176

https://hashcode.co.kr/questions/7560/javascript-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EC%96%B8%EC%96%B4%EC%9D%B8%EA%B0%80%EC%9A%94-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EC%96%B8%EC%96%B4%EC%9D%B8%EA%B0%80%EC%9A%94

https://curryyou.tistory.com/237

https://python-guide-kr.readthedocs.io/ko/latest/starting/which-python.html

https://ralp0217.tistory.com/entry/Python3-%EC%99%80-PyPy3-%EC%B0%A8%EC%9D%B4

https://realpython.com/pypy-faster-python/

https://soooprmx.com/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%80-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0%EC%96%B8%EC%96%B4%EC%9E%85%EB%8B%88%EA%B9%8C/

https://source.android.com/devices/tech/dalvik