2022. 12. 8. 04:11ㆍC언어 복습
1. 함수의 전방 선언에 대해서
우리는 기본적으로 C언어같은 절차지향적인 언어가 위에서 아래로 읽는다는 사실을 배웠을 것이다.
그럼 여기서 다음과 같은 코드가 있다고 가정할 때, 결과가 어떻게 나오는지 맞춰보자.
#include <stdio.h>
int main(void)
{
Func();
return 0;
}
int Func()
{
return 0;
}
대부분의 사람들이 함수의 전방 선언을 배우면 컴파일이 되지 않는다고 이야기한다.
하지만 틀렸다!
놀랍게도 정상적으로 작동을 한다. 이유는 C언어에서 컴파일러는 어떤 함수의 이름을 봤을 때, 이렇게 동작하기 때문이다.
"어? 너같은 함수 본 적 없는데? 너가 어떤 애인지 모르겠지만 프로그래머가 짠 함수라고 가정하고 그냥 넘어갈게. 반환형은 int라고 가정한다?"
놀랍게도 이런 방식으로 동작한다. 물론 경고 메시지를 띄워주긴 하지만 실행 자체에는 문제가 없다. 그리고 실제로 반환형이 int여서 정상적으로 컴파일이 된다. 반대로 Func함수의 int라는 반환형을 다른 자료형으로 바꾸면 자신이 인식한 반환형인 int와 반환형이 달라서 컴파일이 안된다...
참고로 필자가 Visual Studio 2019에서 저 코드 그대로 확장자를 cpp인 파일을 만들어서 복붙을 했을 때는 컴파일이 되지 않았다. 즉, VS 기준으로 C에서는 컴파일이 되었는데 C++ 버전은 저 상황에서 컴파일을 막는 것이다.
2. 빌드 과정의 세부 내용
전체적인 빌드 과정에 대해서는 이전에 공부한 적이 있어서 알고 있었지만 세부적으로 어떤 순서로 이루어지는지 어떤 용어가 쓰였는지 등에 대해서 상세히 기록할 목적으로 적어보았다.
1) 전처리 과정
하나의 소스 파일을 입력 받아서 하나의 트랜슬레이션 유닛(확장된 소스코드)을 반환하는 과정을 말한다.
- 주석 제거
- 매크로 확장 (복붙)
- 인클루드 파일 확장 (복붙)
2) 컴파일 과정
사실 컴파일이라는 단어는 다음과 같은 경우의 상황을 지칭하는 말이다.
- 프로그램 빌드 과정 전체를 지칭하는 경우
- 전처리에서 링크 과정 직전까지를 말하는 경우
- 전처리 이후에 행해지는 컴파일부터 어셈블 과정까지를 말하는 경우
- 전처리 이후와 어셈블 과정 사이를 말하는 경우
이 글에서 말하는 컴파일 과정은 4번째인 전처리와 어셈블 과정을 의미하며 이는 C언어로 작성된 코드를 어셈블리어 단계의 코드로 바꾸는 과정을 의미한다.
즉, 하나의 트랜슬레이션 유닛을 입력 받아서 하나의 어셈블리어 코드로 반환하는 과정을 말한다.
3) 어셈블 과정
간단하게 얘기하면 어셈블리어로 작성된 코드를 기계가 알아들을 수 있는 목적 코드로 만드는 과정이다. 즉, 하나의 어셈블리어 코드를 입력 받아서 하나의 목적 코드로 반환하는 과정을 말한다.
참고로 어셈블리어는 해당 기계에서만 동작할 수 있는 단계의 코드이다. 그래서 C언어 컴파일러가 자동으로 해당 기계의 운영체제, CPU에 맞춰서 그에 맞는 어셈블리어로 변환을 시켜주는 것이다.
하나의 C언어 소스파일이 어셈블리어로 바뀌면 가끔씩 전역 변수, 함수 이름으로 된 레이블을 볼 수 있다. 해당 파일에는 존재하지 않아서 모르지만 미리 빈 칸으로 만들고 다음 단계에서 실제로 존재하는 녀석인지 판단해서 그 때 빈 칸을 채워넣는 방식을 사용하기 때문이다. 그래서 여기까지는 정의되지 않은 심볼을 사용하는 것이 가능한 단계이기도 하다.
4) 링크 과정
링크 과정은 여러 개의 목적 코드 파일을 입력으로 받아서 그것들을 연결하는 작업을 거치고 기계어로 된 하나의 실행 파일을 반환하는 과정을 말한다.
3)에서 사용하는 전역 변수, 함수들이 다른 파일에 있을 경우 그것을 사용할 수 있도록 실제 그것이 존재하는 주소로 연결시키는 작업을 한다. 즉, 다시 말해서 아래의 단계를 거쳐서 빈 칸을 채우는 것이다.
- 여러 개의 파일을 읽으면서 레이블과 레이블이 의미하는 것(이하 실제 내용)의 위치를 기억해 놓는다.
- 코드를 읽으면서 레이블을 발견하면 실제 내용의 위치로 점프하는 코드를 넣는다.
- 그래서 나중에 실행 파일을 실행시키면 레이블이 실제 내용의 위치와 치환되었기 때문에 코드를 읽으면서 전역 변수를 사용하거나 함수 호출이 가능해지는 것이다.
- 만약 링크 과정에서 레이블이 의미하는 실제 내용을 찾을 수 없으면 이 때 링크 에러를 발생시키게 된다. 이것이 정의되지 않은 심볼을 사용했을 때 나타나는 에러이다.
이런 링크 과정은 앞의 3개의 단계와 비교했을 때 큰 차이점이 있다.
"하나의 입력이 아니라 여러 개의 입력을 받아서 처리한다."
그래서 컴파일 과정을 분류할 때, 다른 단계는 몰라도 링크 과정은 반드시 다른 단계와 분리한다.
그리고 이렇게 하면 아래와 같은 장점이 존재한다.
- 여러 개의 C파일 중에 하나의 C파일을 건드렸을 때, 다른 파일까지 다시 컴파일을 할 필요가 없다.
- 해당 파일만 컴파일하고 다른 목적 코드 파일과 같이 합쳐서 링크하면 훨씬 빠르고 효율적이다.
- 소스 파일을 관리하는 것도 훨씬 편해진다. 실제로 업계에서도 소스 파일을 목적 코드 파일을 따로 만들어서 관리하고 그 목적 코드 파일로 실행 파일로 만들어 준다고 한다.
3. 라이브러리
다들 C언어를 처음 배울 때는 빌드가 모두 끝나면 실행 파일이 나오는 그림을 떠올리고는 한다. 하지만 실행 파일말고 다른 파일도 빌드를 시킬 수 있으니 그것이 바로 라이브러리 파일이며 정의는 아래와 같다.
"다른 파일과 링크시킬 목적으로 만드는 하나 이상의 함수 등을 모아놓은 바이너리 파일"
미리 만들어서 빌드를 시켜놓으면 나중에 다른 실행 파일에서 이것을 가져다 쓸 수 있다는 장점이 있다.
그리고 이런 라이브러리 파일은 두 가지 종류가 있다.
정적 라이브러리
실행 파일을 만들 때, 라이브러리가 실행 파일 안에 복사되어 들어가는 방식이다. 다시 말해서 실행 파일을 뽑으면 라이브러리의 내용이 마지막에 합쳐져서 실행 파일에 그대로 존재한다.
장점: 빌드가 될 때, 컴파일러와 링커가 실행 파일을 뽑으면서 미리 정적 라이브러리까지 최적화를 해줄 수 있기 때문에 실행 속도가 동적 라이브러리에 비해 더 빠르다. 또한 미리 만들어 놓으면 누구나 가져가서 컴파일없이 링킹해서 사용할 수 있다.
단점: 실행 파일의 크기가 커지고 메모리를 더 많이 잡아먹을 수 있다.
동적 라이브러리
우리가 빌드 과정에 대해서 배울 때 링크 과정에서 실행 파일을 뽑기 전에 빈 칸을 채워야 한다고 배웠다. 근데 동적 라이브러리를 사용하면 이것과 조금 다른 방식으로 빈 칸을 채우게 된다. 링커가 빈 칸을 채우는 것이 아니라 실행 파일을 실행할 때, 운영체제가 동적 라이브러리를 통해 실시간으로 링킹을 해주는 방식이다. 그래서 정적 라이브러리와 다르게 실행을 할 때까지도 실행 파일과 별개의 파일로 존재하는 것이다.
장점: 실행 파일을 뽑을 때마다 라이브러리의 내용을 실행 파일에 포함시키지 않아도 되기 때문에 여러 실행 파일이 하나의 라이브러리를 공유할 때, 메모리 자원을 아낄 수 있다. 또한 실행 파일 자체의 메모리 크기도 적다.
단점: dll의 버전에 주의하지 않으면 dll 지옥에 시달려야 한다. 그리고 속도 자체는 정적 라이브러리보다는 느리다.
'C언어 복습' 카테고리의 다른 글
POCU C언어 정주행 6회차 - 댕글링 포인터, 포인터 연산, 캐스팅, ASLR (0) | 2022.12.10 |
---|---|
POCU C언어 정주행 4회차 - scope(범위), goto문, const (2) | 2022.12.08 |
POCU C언어 정주행 3회차 - 평가 순서, Sequence Point (0) | 2022.12.08 |
POCU C언어 정주행 1회차 - 매개변수 빈칸 vs void, 주석, signed vs unsigned (0) | 2022.12.02 |
POCU C언어 정주행 - Intro (0) | 2022.12.02 |