C 빌드 과정과 라이브러리

임정우·2023년 8월 7일

C언어 빌드 과정

빌드: 사람이 읽기 쉬운 소스코드를 기계어 명령어로 변환하는 과정

C언어의 빌드 과정은 4단계이다.

  • 전처리
  • 컴파일
  • 어셈블
  • 링크

0. 에디터를 이용하여 소스파일 작성
소스파일은 에디터를 사용해서 작성한 문서파일을 말한다. 에디터를 이용하여 사용하는 언어에 맞게 파일을 작성한다.

1. 전처리
헤더파일을 포함하고, 매크로를 확장하는 단계

2. 컴파일
컴파일은 번역한다는 의미로, 컴파일은 인간이 이해할 수 있도록 작성한 소스파일을 기계가 이해할 수 있는 기계어파일(어셈블리어)로 번역한다.

3. 어셈블
어셈블리 코드를 읽으면서 오브젝트 코드로 만든다. gcc는 as라는 어셈블러를 사용한다.

어셈블리 코드와 오브젝트 코드?

어셈블리어 코드:
어셈블리 코드는 기계어와 거의 1:1대응 되지만 아직 텍스트 파일 형태
어셈블리 코드가 생성된 순간부터는 해당 플랫폼에서만 동작 가능 (이미 자료형 크기 등이 결정된 상태)
어셈블리 코드는 아직 정의되지 않은 변수나 함수를 사용가능

#어셈블리어 코드 예시
lea         edi,[ebp+FFFFFF1Ch]  
mov         ecx,39h  
mov         eax,0CCCCCCCCh  
rep stos    dword ptr es:[edi]  

오브젝트 코드:
기계가 곧바로 이해가능한 기계코드 (기계어)
이진 코드 (0, 1 로만 이루어진 코드)

#오브젝트 코드 예시
00 00 00 04 00 00 00 00 FF FF 5F 61 30 01

4. 링크
링커: 컴파일러가 만들어낸 하나 이상의 목적 파일을 가져와 이를 단일 실행 프로그램으로 병합하는 프로그램
생성된 목적파일을 링커를 이용해서 연결시킨다. 링커를 이용해서 목적파일을 연결시키면 실행파일인 .out 또는 *.exe 파일이 하나 만들어지며 이 파일은 실행이 되는 프로그램이 된다.


목적파일 .obj를 만드는이유

1. 실행 파일을 만드는 시간을 줄여준다.
보통 개발자들은 실행파일 하나를 만들기 위해 소스파일 여러개를 작성한다. 이 때 소스파일1, 소스파일2, ... 소스파일100까지 있다고 할 때, 만약 소스파일1만 수정을 한다면 전체를 다시 컴파일할 필요없이 나머지 소스파일1에 해당하는 목적파일만 교체하면 된다.

2. 분업할 때 유용하다.
나아가서 분업을 한다고 했을 때, 목적파일을 이용하면 하나의 프로그램을 짤 때 여러 명이 각기 다른 기능을 가진 프로그램을 짜도 된다. 이 사람은 A 부분을 짜고, 이 사람은 B 부분을, 이 사람은 C 부분을 짠 뒤에 각기 짠 프로그램을 *.obj 파일로 만들어서 총기획자에게 보내주면 작업이 끝난다. 프로그램 기획자는 A, B, C의 목적파일들을 링커로 연결만 하면 된다.


gcc 명령어

[gcc 컴파일]

1) gcc 파일명(*.c) : Default로 out 파일이 생성된다. (ex a.out)

2) gcc -c 파일명(*.c) : 오브젝트 파일을 생성한다.
( -c 옵션 : as에 의한 어셈블까지만 수행하고 링크는 수행하지 않는다.)

3) gcc -c 오브젝트_파일명(.o) 파일명(.c)

그 외 옵션:
Wall : 모든 모호한 코딩에 대해서 경고를 보내는 옵션
Wextra : Wall에 의해 활성화되지 않는 추가적인 Warning flags를 활성화
Werror : 모든 경고를 컴파일을 중단하는 에러로 취급해서 경고 하나만 나와도 컴파일 중단.
-include 옵션 : 헤더 파일을 소스내에 추가할 때 사용.

더 다양한 옵션은 나중에 사용할 일이 있으면 더 자세히 포스팅하겠다.


라이브러리

  1. 라이브러리란?
    소프트웨어를 개발할 때 컴퓨터 프로그램이 사용하는 비휘발성 자원의 모임,
    문서, 메시지 틀, 미리 작성된 코드, 서브루틴(함수), 클래스, 값, 자료형 사양을 포함

쉽게 말하면 라이브러리를 이용하여 오브젝트파일들을 모아서 하나로 만들어 놓고 필요할 때 같이 링크를 하여 쓸 수 있다.

참고
루틴: 특정한 일을 실행하기 위한 일련의 명령

서브루틴 : 반복되어 사용하는 것을 메모리에 한번 적재하여 여러번 사용할 수 있도록 하는 방법 > 기억장소 절약 가능, 함수안에 함수가 있을 경우에 바로 안쪽의 함수를 서브루틴이라고 부른다.

2. 정적라이브러리와 동적라이브러리

정적라이브러리
정적 링킹(Static Linking) 과정에서 링커가 프로그램에 필요로 하는 부분을 라이브러리에서 찾아 실행 파일에 복사하는 방식의 라이브러리

  • 라이브러리 안에 있는 기계어를 최종 실행파일에 가져다 복사함
  • 실행파일 크기가 커지고 메모리 사용 증가
  • 실행 속도가 빠름

동적라이브러리
동적 링킹(Dynamic Linking) 과정에서 링커가 라이브러리 내용을 복사하지 않고 해당 내용의 주소만 가지고 있다가 런타임에 실행 파일과 라이브러리가 메모리에 위치할 때 해당 주소로 가서 필요한 내용을 가져오는 방식의 라이브러리

  • 실행 파일의 크기가 작음
  • 메모리 자원의 효율적 사용 가능
  • 실행할 때 동적라이브러리 필요
    (동적 라이브러리가 버전이 안맞거나 링크가 안되어있으면 문제 발생 -> 이식성이 낮음)
  • 매번 프로그램 영역에서 라이브러리가 저장된 주소로 이동해야함
    (약간의 성능 감소 가능)

확장자별 라이브러리 구분
.a : 리눅스 / 정적
.so : 리눅스 / 동적
.lib : 윈도우 / 정적
.dll : 윈도우 / 동적

정적 링킹과 동적 링킹

정적링킹
실행 가능한 목적 파일을 만들 때 프로그램에서 사용하는 모든 라이브러리 모듈을 복사하는 방식
링커에 의해 이루어짐

  • 동적링킹에 비해 빠르다.
  • 링킹한 함수가 변하면 다시 컴파일 해야함
  • 중복 발생 가능: 만약 10개의 프로그램에서 A라는 함수를 사용하는데 정적링킹을 사용하면 10개의 목적파일 각각에 A의 정보가 담긴다
    -> 메모리 효율이 좋지않음

동적링킹
실행 가능한 목적 파일을 만들 때 프로그램에서 사용하는 모든 라이브러리 모듈을 복사하지 않고 해당 모듈의 주소만을 가지고 있다가, 런타임에 실행 파일과 라이브러리가 메모리에 위치될 때 해당 모듈의 주소로 가서 필요한 것을 들고 오는 방식,
런타임에 운영체제에 의하여 이루어짐

  • 여러 프로그램에서 A함수를 사용해도 A함수는 하나만 있어도 됨: 각각의 목적파일에서는 A 자체에 대한 정보가 아니라 주소만 가리키고 있기 때문.
    -> 메모리 사용이 적다```
    코드를 입력하세요
- A함수가 변해도 그 변화를 적용하기 위해 다시 컴파일 할 필요가 없음: 이 또한 그 주소를 가리키고 있기 때문
- 매번 주소를 따라가야하기 때문에 오버헤드가 발생하여 정적링킹보다 느리다.

>오버헤드:
프로그램 실행중에 실행시킬 때 **추가적으로 혹은 간접적으로** 필요한 메모리, 시간 등의 자원이 사용되는 현상
예를 들어 어떤 기능을 하는데 10초가 걸렸는데 간접적인 원인으로 20초가 걸렸다면 10초의 오버헤드가 있는 것

정적 라이브러리 만들기

ar 명령어: 여러 오브젝트 파일들을 하나의 아카이브 파일에 저장한다.

아카이브: 여러개의 오브젝트 파일들을 하나의 파일에 묶어 놓은 것

gcc test_code.c obj1.o obj2.o -o test_code

사용 예시)

아카이브(라이브러리) 만들기

ar rscv libtest.a file1.o file2.o

ar 옵션:
r : replace, 새로운 오브젝트 파일이면 추가, 기존 파일이면 치환
c : 아카이브(라이브러리 파일) 생성, 존재하지 않는 아카이브를 작성(또는 갱신)하는 경우에도 경고 메시지를 출력하지 않음
u : update files, 오브젝트 파일의 타임스탬프를 비교해 새로운 파일일 경우에만 치환
s : ranlib와 마찬가지로 아카이브 인덱스 생성. 아카이브 인덱스를 생성하지 않으면 링크 속도가 느려지고, 시스템 환경에 따라 에러가 발생할 수도 있음
v : 자세한 내용을 보여주는 verbose 모드로, 이 옵션을 이용하면 파일 크기나 갱신 시각 등의 상세 정보도 출력

gcc로 정적라이브러리와 함께 컴파일

gcc -I<헤더파일 경로> -L<라이브러리 경로> -ㅣ<라이브러리명> -o <파일명> 

gcc 옵션 정리

-o <file> 
output을 파일로 만든다.

-I<directory>
include 파일들에 필요한 경로를 추가한다.

예제

test1.c

  #include <stdio.h>
void test1()
{
	printf("test1\n");
}

test2.c

#include <stdio.h>
void test2()
{
	printf("test2\n");
}

test_code.c

void test();
void test2();

int main()
{
	test1();
	test2();
}

컴파일 후 아카이브로 만들기

gcc test1.c -c -o test1.o
gcc test1.c -c -o test1.o
ar -rscv libtest.a test1.o test2.o  

라이브러리와 함께 test_code.c 컴파일

gcc -o test_code test_code.c -L. -l test

-L. : -L은 라이브러리 파일이 위치한 경로를 의미하므로, -L.은 현재 위치를 가르킨다.
-l test : 라이브러리파일을 가르키고 libtest.a를 가르킨다.

이 결과는

gcc test_code.c test1.o test2.o -o test_code

와 동일하다.
만약 같이 컴파일해야할 목적코드들이 수없이 많아진다면 이와 같은 형태로 작성하기 힘들 것이다.

profile
경희대학교 소프트웨어융합학과

0개의 댓글