[C] C lang 실행과정

C 프로그램에서의 빌드 과정

  • 빌드란
    • 사람이 읽기 쉬운 명령어를 기계어로 변경하는 과정
    • 명령어들을 모아 기계에서 실행 가능한 파일로 만드는 것.
  • C의 빌드란(과정 전체 통틀어서 빌드라 일컫는다.)
  1. 전처리
  2. 컴파일
  3. 어셈블
  4. 링크


20221010_112700

중간 초록 부분은 어떤 부분은 따로 봐서 컴파일링 어셈블링을 따로 보기도 하고 어떤 부분은 합쳐서 컴파일링이라 보는 곳도 있기는 하다.



보통 아래와 같이 빌드함.

  • clang -std=c89 -W -Wall -pedantic-errors *.c

  • 그럼 clang이 알아서 4단계를 실행해준다.
    • 결과는 최종 실행파일 a.out, a.exe파일을 20221010_123107
  • 물론 clang에서 한단계 씩도 진행 가능은 함.

20221010_123918



.h, .c파일

20221010_124353

20221010_124505

.c 파일이 실제 우리가 쓰는 일반 소스코드라 보면 된다.

실제 프로그램을 돌게하는 코드들. 로직코드 저장해두는게 c파일 이걸 구현한 것을 정의라 한다.


헤더파일은 여러소스코드에서 공유하고 싶은 코드들이 있는데 이걸 저장해둠

함수 원형을 저장해둠.

20221010_124740



헤더파일이 필요한 이유?

  • 반드시 필요하진 않음. 근데 좀 더 효율적으로 구조 잡고 작성하려면 파일 분리해야 하는데 C는 워낙 고대언어라 이걸 문제없이 지원하기 힘듬.

  • 만약 동일한 함수 써야된다고 동일한 함수를 복붙하면 되지 않나?

    • 함수나 코드 중복은 엄청난 죄악
    • 유지보수 어쩔껀데..

근데 함수 선언만으로 어떻게 프로그램이 도나?

  • 컴파일 단계를 자세히 보면 됨.

  • 빌드가 여러 단계로 쪼개져 있는 이유가 정의 없이 선언만 가지고도 컴파일 되게 하기 위함.

  • 실제 올바른 기능 호출은 링크 단계 이후에 이뤄짐.



include <> 와 include “”

  • 인클루드 하는 방법은 2가지가 있음.
  • 이 둘의 차이는 디스크 상에서 어디서 파일을 찾냐 의미.


#include <>

  • <>는 시스템 경로에서만 헤더 파일을 검색
    • 보통 컴파일러가 제공하는 시스템 헤더파일을 인클루드 할 때 사용.

#include “”

  • "”는 현재 작업중인 디렉토리에서 헤더파일을 먼저 검색한 뒤 없으면 시스템 경로를 검색

    • 개발자가 구현한 헤더파일들을 인클루드 시 사용.



빌드과정 1: 전처리 단계

20221010_132145

  • 보통 전처리기(processor) 라는 별도 프로그램이 담당.

  • 전처리 단계 : 입력 - 처리 - 출력
    • 먼저 주석 제거함.
    • 매크로 (복붙) 확장 함.
    • #로 붙는 매크로들 다 확장한다 보면 됨.

    • 인클루드 파일들을 확장하고 그 자리에 헤더파일 있는 내용들을 가져다 붙인다.
  • 출력 : 확장된 소스코드
    • 컴파일의 기본단위인 트랜슬레이션 유닛(translation unit)



트랜슬레이션 유닛 보는 법

  • clang -std=c89 -W -Wall -pedantic-errors -E adder.c

clang 컴파일 중 -E 플래그를 넣으면 됨.

전처리기까지 돌리고 결과 보여줌.

20221010_144803


  • clang -std=c89 -W -Wall -pedantic-errors -E adder.c > adder.pre

파일로 저장하려면 출력 리디렉션을 쓰면 된다.


빌드과정 : 컴파일 단계

  • 컴파일러라는 프로그램이 담당.

20221010_150638

  • 어셈블리어는 기계어와 거의 1:1로 대응(하드웨어와 아주 가까움.)

  • 그러나 텍스트 파일이라 여전히 사람이 읽기 쉬운언어(그나마)

20221010_151237



어셈블리어 코드는 아직 정의를 모르는 심볼 사용이 가능

  • 심볼: 함수나 변수 이름 등
  • 이것이 헤더를 통한 선언만으로 컴파일이 가능한 이유

컴파일러가 어떤 함수나 변수의 정의를 못 찾을 경우

  • 선언만 보고 다음과 같이 행동함

20221010_151431

이걸 메꾸는 게 링크 단계가 해줌.


  • 어셈블리어 코드 보는 방법
    • 컴파일 플래그 -S를 쓰면 된다. 어셈블리어 코드가 .s파일로 저장됨.

    • clang -std=c89 -W -Wall -pendatic-errors -S adder.c



어셈블리어 코드가 나왔다는 의미?

  • 이 단계 부터는 코드는 특정 플랫폼에서만 동작
  • C가 크로스 플랫폼이라는 주장은 컴파일 되기 전 까지임.
  • 또 타겟 플랫폼이 몇 비트냐에 따라 C 자료형 크기가 달라질 수 있음.
    • 어셈블리어 코드는 이미 그 자료형 크기가 결정된 후
  • 64비트 OS에서는 32비트 프로그램을 실행할 수 있지만 그 반대는 안됨.



빌드 과정: 어셈블 단계

  • 어셈블러라는 프로그램이 담당

20221010_153851

  • 오브젝트 출력

    • 기계가 곧바로 이해 가능한 코드.
    • 기계어라고 한다.
    • 즉, 이진코드
    • 어셈블리어와 마찬가지로 메꿔야 하는 구멍이 있음.



오브젝트 코드는 -c 플래그를 넣어서 컴파일 하면 .o 파일로 컴파일 됨.

  • clang -std=c89 -W -Wall -pendatic-errors -c main.c

오브젝트 파일은 이진파일이므로 메모장이나 텍스트 툴로 열면 깨져서 안보임.

Hex editor같은 특별한 에디터를 써야 보인다.(16진수 편집기.)

20221010_155523

20221010_155630



빌드과정 : 링크 단계

  • 링커라는 프로그램이 담당.

  • 입력 : 모든 오브젝트 코드들.

  • 링커는 모든 오브젝트 코드들을 모아다 구멍을 메꾼뒤 실행 파일로 저장.

20221010_160015

20221010_160026



  • 만약 선언만 믿고 사용한 함수나 변수가 여전히 구멍으로 남아있다면? (즉, 다른 오브젝트 코드에서 정의를 못 찾았다면?)

    • 링커가 못 찾는다며 링커 오류 뱉음.
    • 그 함수나 변수가 없어 실행할 방법이 없기에 경고가 아니라 오류.

    20221010_160510

위 에러는 선언만 해놓고 실제 구현을 제대로 안 해놔서 나오는 에러


링크단계가 다 끝나면 출력으로 최종 실행파일을 출력한다.(.exe, .out)


링크 단계가 분리되어 있는 이유

왜 굳이 링크 단계가 분리되어 있는가

  • 사람들은 보통 컴파일(처음 3단계) 와 링크, 두단계로 나눠서 생각.

20221010_172114

수많은 구멍을 컴파일 할 때마다 메꾸기엔

  • 조금 전 예에서 보듯 .c파일이 많으면 구멍 메꾸는 일이 매우 복잡
  • 예: .c 파일이 수천인 프로젝트에서 .c 프로젝트 하나 컴파일 할 때마다 모든 함수 찾아서 구멍 메꿔야 하나?

  • 여러개의 .c 파일에서 동일한 외부 함수를 사용할 경우, 최종 실행 파일에 그 함수 정의가 중복으로 들어가는 것도 막아야 함.
    • 이 중복을 체크하려면?
  • 그럼 모든 c파일을 한번에 합쳐서 컴파일 하면 되지 않나?
    • .c파일 하나만 수정해도 모든걸 다 컴파일 해야하는데?
  • .c 파일 하나씩 따로 컴파일해서 오브젝트 파일로 저장해두는 방법이 낫다.

    • 나중에 바뀐 .c 파일만 컴파일해서 새로운 오브젝트 파일 생성
    • 기존에 있던 오브젝트 파일과 합쳐서 링크
    • 훨씬 빠르다.
    • 분리 시켜 놓으니 간단해짐. 할 일 분리되고



근데 .o 파일로 어떻게 .exe파일을 만들지?

  • 실제로 업계에서 가장 흔한 방식
  • vs에선 기본적으로 오브젝트 파일과 .exe파일을 모두 만들어 줌.
  • clang은 그냥 바로 .exe파일만 만들어줄 뿐
  • clang에서 어떻게 .o 파일로 .exe파일을 만들 까

  • clang -std=c89 -W -Wall -pedantic-errors main.o adder.o //이런식으로 .o파일을 명시하면 된다.

  • clang -std=c89 -W -Wall -pedantic-errors *.o



라이브러리, 정적/동적 라이브러리와 링크

라이브러리로도 빌드 할 수 있다.
  • 참고로 빌드 결과가 실행파일이 아니라 라이브러리 파일이 나오게 할 수 도 있다.

  • 라이브러리란?
    • 위에서 본 함수들을 기계어로 변환 후, 파일 하나로 저장해놓은 것.
    • 나중에 다른 .c 파일에서 이 기능이 필요 시 , 같이 링크해서 쓸 수 있음.
  • 라이브러리는 2 종류가 있음.
    • 정적 라이브러리
    • 동적 라이브러리



정적 라이브러리와 링크

  • 정적 라이브러리와 링크하는 것을 정적 링킹이라고 함

  • 라이브러리 안에 있는 기계어를 최종 실행파일에 가져다 복사함.
  • 동적 링킹에 비해
    • 실행파일의 크기가 커짐.
    • 메모리를 더 잡아먹을 수 있음.
    • 실행 속도가 빠름.

    20221010_174637


동적 라이브러리와 링크

  • 동적 라이브러리와 링크 하는 것을 동적 링킹이라고 함
  • 실행파일 안에 여전히 구멍을 남겨두는 방법
  • 실행파일을 실행할 때 실제로 링킹이 일어남
    • 이 링킹은 실행 주엥 운영체제가 해줌.

    20221010_181640



dll 파일?

  • C#등에서 .dll파일 본적 있다면 이게 바로 그것
  • dll은 dynamic link library 의 약자

20221010_182144

  • 정적 링킹에 비해
    • 실행파일 크기가 적다
    • 여러 실행 파일이 동일한 라이브러리를 공유 가능하다 -> 메모리 절약
    • 여러 실행파일이 이름은 같지만 버전이 다른 동적 라이브러리를 사용시 DLL 지옥을 볼 수 있다.
      • 보안 프로그램을 A은행에서 깔고 B은행이 크래시 나고 B은행에서 깔면 A은행에서 크래시 나고 이런 경우



분할 컴파일과 전역 변수

  • 앞에서 2개로 나는 소스코드.
  • 하지만 굉장히 큰 프로젝트면 2개로 안됨.

    • 프로그래머가 100명이면?
    • 대부분 비슷한 기능 모아 기능멸로 파일에 저장하는게 좋다.
    • 모두가 한 파일을 고치면 매우 힘들어 짐.
      • 서로 고쳐서 충돌 나느거 붙어 컴파일 시간까지?
    • 그럼 여러개 파일이 어떻게 컴파일 되는지 봐야함.



분할 컴파일과 전역변수

  1. 2개 이상의 .c파일을 개별적으로 컴파일해서 오브젝트 파일을 만듦
  2. 오브젝트 파일들을 연결시켜(링크해서) 실행파일을 만듦.

이 2개를 3개, 4개로 늘리는건 어려운 일이 아니다.

  • 하나 컴파일 할때 컴파일 하는 프로그램 돌리고, 얘 컴파일 할 떄 또 한번 돌리고, 링크는 다 모아서 한번에 만드는 거.

20221011_105851

기억해야 할 건 파일들 개별적으로 따로따로 컴파일이 된다.



다른 파일에 있는 전역 변수 사용 시 문제점

위는 컴파일 시 오류난다 왜? 선언되지 않은 identifier. 심볼이라 생각하자. 선언되지 않은 이름 썼다. g_mob_count 모른다는 얘기

컴파일 오류가 나는 이유?

  • 컴파일러가 따로 .c 파일들을 컴파일 하기 때문
  • main.c는 monster_repo.c안에 있는 g_mob_count의 존재를 모름.

20221011_134145

이런식으로 바꾸면 되지 않을까? -> 링커가 굉장히 실망.

20221011_134259

컴파일은 되는데 g_mob_count을 찾아왔는데 똑같은 게 main.o에도 있다 그럼 뭘 실행한 건지 애매해짐.

20221011_140106

그래서 C는 동일한 이름을 가진 전역변수를 여럿 만들 수 없다.



왜 링커 오류가 나는건지?

  • monster_repo.c에도 g_mob_count가 있고 main.c에도 있음

  • 컴파일 동안은 서로 모르니 잘 됨.
  • 링크하려고 하니 전역 범위에 같은 이름 쓰는 애가 둘이나 있음.

-> 오류

다른 방법이 필요하다

  • 따라서 새로운 전역변수 만드는 게 아니라 monster_repo.c에 있는 것을 가져다 쓸거라는 걸 컴파일러에게 말해줘야 함.
    • 그래야 컴파일러가 구멍을 비워둠
    • 함수 전방선언이 그랬듯이.



extern 키워드

20221011_143805

  • 다른 파일에 있는 전역변수에 접근하려면 extern 키워드를 사용
  • 어찌보면 C#의 public 접근 제어자라고 생각할 수도 잇지만
    • extern은 그걸 가져다 쓰려는 놈이 맘대로 쓸 수 있음
    • C는 접근 제어자가 없다.
  • extern 쓰려면 C 파일 다 일일이 열어서 전역변수를 찾아야 하나?

extern 사용법

  • 그래도 됨
  • 그런데 어떤 경우는 .c 파일을 볼수 없을 수도 있음
    • 예: 남의 라이브러리는 .c대신 라이브러리 바이너리 파일과 헤더만 줌
  • 이런 경우 라이브러리 제작자가 extern을 아예 헤더에 포함시켜줌
  • 인클루드는 그냥 복붙이니 결과적으로 .c에 넣는 것과 같음.

  • 여전히 c 파일 안에 직접 extern을 넣는 경우가 많음.
  • 헤더에 넣는 것과 c파일에 넣는 것의 차이
    • 헤더에 넣는 것은 누구라도 쓸 수 있게 만드는 것
    • c파일에 넣는 것은 그 파일 안에서만 쓰려는 것.
이렇게 해도 링커 오류 난다.

20221011_144445

  • g_mob_count가 monster_repo.c에 복붙된 뒤 오브젝트 코드로 컴파일 됨.
  • g_mob_count가 main.c에 복붙된 두 오브젝트 코드로 컴파일 됨.
  • 링크가 이 둘을 합치려다 중복된 전역변수를 발견.
그렇다면 함수는?
  • 이미 이전에 봤음. 함수 프로토타입(=선언)

    • 함수 앞에 extern을 붙일 수 있으나 그냥 선언을 하면 자동으로 extern
    • 굳이 extern 없어도 함수 뒤에 { 안 열고 그냥 ; 로 끝나니 쉽게 알 수 있음
    • 그래서 보통 extern 키워드 안 씀
  • 함수 프로토 타입 넣는 법(2가지)
    1. 사용할 곳에서 호출 전 직접 원형을 넣음
    2. 헤더파일에 넣어줌.
  • 함수의 경우 헤더파일에 넣는 방법을 많이 씀.

전역변수의 문제

  • 전역변수 쓰지 말란 말 많이 함.
  • 확실히 문제는 있음
    • extern 사용하면 아무데서 확인 가능하고
    • 심지어 지 맘대로 내 파일 안의 변수 바꿈
    • 파일이 2만개 있으면 어떤 코드가 전역변수 바꾸는지 어케 알지?
  • 다른놈이 내 전역변수 못 쓰게 하려면?
  • 즉, 내 파일에서만 쓰려면?
  • 전역변수 만들 때 static 키워드를 붙여주면 됨.

20221011_152808

20221011_152851


static 키워드

  • 다른 파일에서 전역변수에 접근 못하게 막는 법
  • 이 변수의 범위가 파일로 한정됨.
  • 흔히 정적변수라 함.
  • 여전히 전역변수로, 프로그램 실행동안에 실제 공간을 계속 차지하고 있음.
  • static 변수를 다른파일에서 접근하려면 링커오류

  • static과 전역변수와 다른건 접근을 어디서 하냐 그게 끝.

20221011_155322


static 키워드의 또 다른 예

로컬변수, 지역변수에도 static을 넣을 수 있음.

  • static이 없으면, 지역변수. 함수 반환 시 그 변수도 사라짐.
  • static을 쓰면 개념상 전역변수. 허나 그 함수 안에서만 접근 가능
    • 즉 함수가 반환되도 여전히 값은 저장되어 있음.

20221011_160120



.c와 .h 파일 정리, 순환 헤더 인클루드와 해결법

  • 빌드의 4단계가 올바로 돌게 하려면 아래의 기본 원칙을 따라야 함
    1. 헤더파일에는 선언만 들어간다
    • 함수 선언
    • 전역변수 extern 선언
  1. .c파일에는 정의가 들어간다
    • 함수정의
    • 전역 및 정적 변수 정의



이런 상황을 생각해보자.

20221011_183831

이 때 컴파일을 하면?(컴파일 대상은 c.c만)

20221011_184528

무한 반복.. 컴파일러가 보다 못해 멈춰줌.

이를 순환 헤더 인클루드 라고 한다. 헤더 꼬였다고 말하기도 한다.



해결법 1. 이러한 상황을 최대한 피할것.
  • #include는 가능하면 .c에서만 하기
  • b헤더에서 a헤더를 인클루드 하는 대신에 a에 정의된 것을 전방선언하기
  • 하지만 어쩔수 없이 헤더파일을 서로 인클루드 해야할 일이 있음
    • b.h에서 a.h에 정의된 #define이 필요할 경우
해결법 2. 인클루드 가드
  • C에서 헤더파일이 여러번 인클루드 되는 걸 막는 업계 표준

20221011_190352

위는 ifndef not해주다 def = defined 만약 어떤게 define되지 않았다면 FOO_H로 정의한다.

그리고 아래 헤더파일 내용 적고 endif해줌.

  • #으로 시작하면
    • 전처리기 지시문
    • include 말고 다양한 것이 있음
    • #define #ifdef #ifndef #endif
  • #define, #ifndef, #endif이 제일 많이 사용



인클루드 가드 작동법

  • 전처리기 지시문은 코드 컴파일 전 전처리기가 지시
  • 이떄 1) 어떤 상수를 정의하고, 2)컴파일러엑 ㅔ조건적으로 컴파일 하라고 지시.

20221011_192844


그럼 둘중에 뭐 써야 하나?
  • 해결법 1과 2 둘다 써야함
    • 1 아니면 2가 아니다
  • 언제나 #ifndef/ #define/ #endif 해야하나?
    • 그렇다. C에서 제공하는 라이브러리도 이미 이거 사용
    • 그러니 우리도 우리가 쓰는 코드에만 사용해야함



pragma once 라는 거도 있다던데

  • 인클루드 가드보다 간단해 보이는데 그거 써도 되는지
    • 표준 아님
    • 그냥 최신 컴파일러가 대체적으로 다 지원하는 것
    • 포팅 생각하면 예전 컴파일러 및 시스템과의 호환을 위해 그냥 인클루드 가드 쓸 것.



C 컴파일러의 종류와 특징

  • GCC
    • GNU C 컴파일러는 1987년 출시
    • 리눅스/유닉스 기반 플랫폼에서 주로 쓰던 컴파일러
    • 다양한 C표준을 대부분 제대로 지원
  • MS visaul C++

    • 원래 vs에 딸려오던 C++ 컴파일러이나, 확장자가 .c일 경우 C로 컴파일과
    • C99표준
      • 그러나 모든 표준을 지키지는 않음.
    • C11의 대부분을 지원하지 않음
    • 윈도우 기반 플랫폼에서 이것을 주로 사용
  • Clang
    • LLVM 컴파일러 구조를 사용하는 C언어 계열 컴파일러 프론트엔드
      • 원래 애플사가 개발함. 그 뒤 오픈소스가 된 뒤 마소, 구글 등 다양한 대기업들이 참여
    • GCC 컴파일러 대신 Clang을 쓰면 코드 변경 거의 없이 그대로 컴파일 되고 빠른 컴파일 속도와 LLVM 구조가 제공하는 유용한 기능 덕에 많은 gcc 사용자들이 clang으로 이주중
    • clang-ci라는 비쥬얼c와 호환되는 프론트엔드 제공.
    • 기타 소형기기 전용 컴파일러도 다수 존재.






© 2021.03. by yacho

Powered by github