하고 싶은 프로젝트의 레퍼런스 프로젝트가 C언어로 되어 있어서 요즘 그것을 분석하기 위한 시간을 보내는 중이었다.

빨리 하고 싶어서 모르는 상태로 챗 gpt에 물어보고 나오는 명령어를 급하게 생각 없이 옮겨 쓰다보니... 문제가 더욱 커졌다. 이번 기회로 gpt에 대한 의존은 좋지 못하다는걸 몸으로 깨닫게 되었다. 오히려 ai를 현명하게 쓰지 못할 경우 시간 낭비가 더욱 발생하게 된다.

그럼 본론으로 들어가보려고 한다.

 왜 Makefile과 make가 필요한가?

이 이슈에 대한걸 그대로 구글링을 해보았다.

https://www.includehelp.com/c-programming-questions/what-is-makefile.aspx

 

What is Makefile for c program compilation and how to create Makefile? - IncludeHelp

What is Makefile for C program compilation and How to create Makefile? Makefile in Linux for Compilation If you have multiple source files in c, c++ and others language and want to compile them from Terminal Command, it is hard to write every time. To solv

www.includehelp.com

이런 사이트를 찾을 수 있었다. 그 내용을 살펴보자.

C 프로그램 컴파일용 Makefile이란 무엇이며 Makefile을 만드는 방법은 무엇입니까?

컴파일을 위한 Linux의 Makefile

Makefile은 컴파일 프로세스를 단순화하거나 구성하는 데 사용되는 도구입니다. Makefile은 목적 파일을 만들고 제거하기 위한 변수 이름과 대상 명령어 집합으로 구성됩니다. 하나의 Makefile에서 여러 개의 대상을 만들어 이진 파일을 컴파일하고 제거할 수 있습니다. Makefile을 사용하여 프로젝트(프로그램)를 여러 번 컴파일할 수 있습니다.

c, c++ 등 여러 언어로 된 소스 파일이 여러 개 있고 이를 터미널 명령에서 컴파일하고 싶다면 매번 작성하기가 어렵습니다. 이러한 종류의 문제를 해결하기 위해 Makefile을 사용하는 이유는 대규모 프로젝트를 컴파일하는 동안 많은 수의 소스 파일을 작성해야 하고 링커 플래그가 필요하기 때문에 반복해서 작성하기가 쉽지 않습니다.

이 짧은 문장을 보고 든 생각을 정리해보았다.

1. C 프로그램 컴파일 이라함은 구체적으로 어떤 것을 의미하는가? 어떤 확장자를 해석해서 어디로 전달하고 무엇을 얻을 수 있는가?

2. Makefile 은 만들 수 있는 무언가다.

3. Makefile은 텍스트 기반 인터페이스에서 컴파일하는 과정에 도움을 주는 무엇인가다.

4. C 혹은 C++ 환경에서 사용된다.

5. "링커 플래그"는 무엇인가?

6. 많은 수의 소스 파일을 작성하는 수고를 덜어줄 수 있다.


여기서 C언어를 컴파일 하려면 어떻게 해야하는지를 잠시 언급하자면

GCC 혹은 G++ 등을 통해 C, C++ 파일을 컴파일하게 된다.

여기서 컴파일 하는 과정은 다음과 같다.

gcc 소스 파일 이름

 

 

이어서 밑의 설명을 보자.

Makefile 은 컴파일을 위해 코드를 단순화하거나 구성하는 도구입니다. Makefile 은 개체 파일을 만들고 제거하기 위한 변수 이름과 대상이 있는 명령 집합(터미널 명령과 유사)입니다. 단일 make 파일에서 우리는 바이너리 파일을 컴파일하고 제거하기 위해 여러 대상을 만들 수 있습니다. Makefile 을 사용하여 프로젝트(프로그램)를 여러 번 컴파일할 수 있습니다 .

예를 들어 이해해 보겠습니다. main.c (메인 소스 파일), misc.c (함수 정의를 포함하는 소스 파일), misc.h (함수 선언을 포함)
3개의 파일이 있다고 가정합니다 . 여기서 myFunc() 라는 함수를 선언하고 정의하여 무언가를 인쇄합니다. 이 함수는 각각 misc.c  misc.h 에서 정의되고 선언됩니다 .

myFunc() 는
misc.h(헤더 파일)에서 선언되었고
misc.c(소스 파일)에서 정의되었습니다.
main.c 에서는 사용됩니다.

이러한 파일들을 컴파일하려면 명령어를 계속해서 입력해주어야 합니다. 아때 Makefile을 사용하면 자동으로 명령어를 실행하여 컴파일을 수행합니다. Makefile을 사용하면 프로그램을 여러번 컴파일해야할 때 일일이 명령어를 입력하는 수고를 덜 수 있습니다.

Makefile을 하게 되면 object 파일들을 링크해서 실행 파일을 만들게 됩니다.

더 자세한 내용은 후속편에서 다루도록 하겠습니다.

깃허브에서 받은 프로젝트를 돌려보기 위해서 readme.md를 읽고
프로젝트를 분석해서 라이브러리들을 빌드하고, make를 해봤으나....!!
엄청 긴 에러 로그를 만나게 되었다.
처음엔 원인이 무엇인지도 모르고 뻘짓을 많이 하긴했었지만, 결국 에러 로그는 무언가 잘못한게 있기 때문에 나오는 것이니까 하나하나 파헤쳐보던 중에 알게 된 사실이 있다.

"C언어 makefile에서 라이브러리를 링크하는 과정에서는 그 순서도 정말 중요하다."

그래서 같은 라이브러리를 import 한다고 하더라도 그 순서에 따라서 빌드가 성공할수도, 실패할수도 있는 것이다.

이게 내가 기획하고 처음부터 빌드한 프로젝트면 몰라도 다른 사람이 만든 프로젝트를 다운 받아서 빌드해볼 때는 꽤나 자주 겪을 수 있는 문제라 기록을 남기려고 한다.

 ldd - 실행 파일 또는 공유 라이브러리 파일의 의존성 체크

 

"ldd program_name" 명령어를 실행하면 program_name이 의존하는 라이브러리들을 확인할 수 있습니다. 
일반적인 경우 ldd는 LD_TRACE_LOADED_OBJECTS 환경 변수를 사용하여 표준 동적 링커를 호출합니다.
이렇게 하면 동적 링커가 프로그램의 동적 종속성 및 찾기를 실행하여 이를 충족하는 객체를 로드합니다.

ldd가 a.out 공유 라이브러리에서 작동하지 않는다는 이슈를 확인했습니다.
이는 매우 오래된 일부 a.out 프로그램에서 작동하지 않기 때문에 언급되었던 이슈입니다. ldd 자원이 컴파일러 릴리스에 추가되기 전에 빌드 되었습니다. 

일부 ldd 버전은 종속성 정보를 얻으려고 시도할 수 있습니다.
그렇기 때문에 신뢰할 수 없는 실행 파일에 ldd 를 사용해서는 안됩니다. 보안 이슈가 있습니다.

ldd에도 옵션이 존재합니다.

--version ldd 
              의 버전 번호를 인쇄합니다 .

       -v , --verbose
              예를 들어 기호를 포함한 모든 정보를 인쇄합니다.
              버전 정보.

       -u , --미사용
              사용하지 않는 직접 종속성을 인쇄합니다. (glibc 2.3.4부터.)

       -d , --데이터-재배치
              재배치를 수행하고 누락된 개체를 보고합니다(ELF
              오직).

       -r , --function-relocs
              데이터 개체와 기능 모두에 대한 재배치를 수행합니다.
              누락된 개체 또는 기능을 보고합니다(ELF만 해당).

       --help 사용 정보.

 

https://man7.org/linux/man-pages/man1/ldd.1.html

참조

 

ldd(1) - Linux manual page

ldd(1) — Linux manual page LDD(1) Linux Programmer's Manual LDD(1) NAME         top ldd - print shared object dependencies SYNOPSIS         top ldd [option]... file... DESCRIPTION         top ldd prints the shared objects (shared libraries) r

man7.org

 

 readelf - ELF 파일이 의존하는 라이브러리의 정보 확인

ELF(Executable and Linkable Format)는 실행 파일, 목적 파일, 공유 라이브러리 그리고 코어 덤프를 위한 표준 파일 형식이다. 1999년 86open 프로젝트에 의해 x86 기반 유닉스, 유닉스 계열 시스템들의 표준 바이너리 파일 형식으로 선택되었다. - 위키 백과

ELF 파일은 코드와 데이터를 여러 섹션으로 구분하고, 각 섹션은 읽기 전용, 쓰기 가능 또는 실행 가능 등의 특정 권한을 가질 수 있습니다. 또한 ELF 파일은 컴파일된 코드 및 데이터 외에도 기호(symbol)와 같은 디버그 정보를 포함할 수 있으며, 동적 링크를 지원하기 위해 필요한 링크 섹션을 가지고 있습니다.

ELF에 대해서는 공부하고 정리할게 많아서 추후에 한번 더 정리할 계획이다.

 

 objdump -  GNU 바이너리 유틸리티의 일부

objdump 는 GNU 바이너리 유틸리티의 일부로서, 라이브러리, 컴파일된 오브젝트 모듈, 공유 오브젝트 파일, 독립 실행 파일 등의 바이너리 파일들의 정보를 보여주는 프로그램이다. objdump는 개체 파일 및 실행 파일을 검사하는데 일반적으로 사용되는 command line tool 입니다. objdjump 는 ELF 파일을 어셈블리어로 보여주는 역어셈블러로 사용될 수 있습니다. 역어셈블러는 기계어를 어셈블리어로 변환하는 컴퓨터 프로그램으로서, 리버스 엔지니어링 도구 중 하나로 볼 수 있는데, 이번 포스팅에서는 생략하도록 하겠습니다.

objdump 를 사용해서 얻을 수 있는 이점 중의 일부를 3가지 정도만 언급하겠습니다.

1. 디버깅 : objdump를 사용하여 실행 파일의 내용을 검사하고 기호 누락, 잘못된 재배치 정보 또는 프로그램이 충돌하거나 잘못 작동할 수 있는 기타 문제와 같은 문제를 찾을 수 있습니다.
2. 리버스 엔지니어링  : objdump를 사용하여 바이너리를 분석하고 작동 방식을 학습할 수 있습니다. 이 도구는 프로그램의 제어 흐름을 이해하고 기능과 변수를 식별하며 보안 취약성을 감지하는 데 도움이 될 수 있습니다.
3. 최적화 : objdump는 프로그램의 성능을 분석하고 코드의; 병목 현상을 식별하는 데 사용할 수 있습니다. 디스 어셈블리를 검사하여 자주 실행되는 코드 섹션을 식별하고 속도를 위해 최적화할 수 있습니다.

그러나 objdump 를 사용하면 몇가지 일어날 수 있는 문제도 있습니다.

1. 복잡성 : objdump 는 바이너리 파일에 대한 많은 정보를 제공하지만 이 정보를 해석하는 것은 어려울 수 있습니다. 기본 아키텍쳐 및 어셈블리 언어에 대한 충분한 이해가 필요합니다.
2. 보안 위험 : objdump 는 바이너리를 리버스 엔지니어링 하는 데 사용할 수 있으므로 공격자가 취약성을 식별하거나 바이너리에서 중요한 정보를 추출하는 데 사용할 수도 있습니다.
3. 호환성 : 다른 버전의 objdump는 이진 파일에 대해 다른 정보를 제공할 수 있습니다. 분석 중인 바이너리와 일치하는 올바른 버전의 objdump를 사용하는 것이 중요합니다.

간단한 예제와 함께 실습해보겠습니다.

file1.c

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

int main() {
   printf("Hello, World!\n");
   printf("The value of a is %d\n", a);
   return 0;
}


header.h

int a;


Bash

$ gcc -c file1.c
$ gcc -c header.h
$ objdump -p file1.o

 

이렇게 빌드하면 다음과 같은 출력이 나타납니다.

file1.o:     file format elf64-x86-64

.......

 

 pkg-config - 컴파일러나 링커에 필요한 라이브러리 정보를 확인

pkg-config는 C/C++ 소프트웨어 개발에서 사용되는 라이브러리 의존성 관리 도구입니다. 이 도구는 라이브러리의 이름, 버전 및 기타 정보를 검색하여 컴파일러 및 링커가 라이브러리를 찾을 수 있도록 도와줍니다. pkg-config는 소프트웨어 패키지 설치를 간편하게 하기 위해 GNU Build System과 함께 자주 사용됩니다.

pkg-config는 각 라이브러리의 .pc 파일을 읽어서 필요한 헤더 파일, 컴파일러 옵션, 링커 옵션 등을 찾아줍니다. 개발자는 pkg-config를 사용하여 필요한 라이브러리를 검색하고, 해당 라이브러리의 정보를 얻을 수 있습니다. 이를 통해 컴파일 및 링크 단계에서 필요한 라이브러리를 찾을 수 있습니다.

pkg-config를 사용하면 의존성이 있는 라이브러리를 찾기가 쉬워집니다. 또한, 여러 라이브러리를 사용할 때 각 라이브러리의 정보를 일일이 찾아볼 필요가 없습니다. 하지만, pkg-config를 사용하면 각 라이브러리의 .pc 파일이 존재해야 하기 때문에, 라이브러리 제작자가 .pc 파일을 작성하지 않았다면 pkg-config를 사용할 수 없습니다. 또한, pkg-config를 사용하면 Makefile 등의 빌드 스크립트를 복잡하게 만들 수 있습니다.

pkg-config는 3가지 정도의 사용법이 있습니다.

1. 라이브러리 설치 여부 확인
    pkg-config --exists <library-name>
    이 명령은 지정된 라이브러리가 설치된 경우 종료상태 0을 반환하고 그렇지 않은 경우 0이 아닌 값을 반환합니다.

2. 라이브러리에 대한 컴파일러 및 링커 플래그 검색
    pkg-config --cflags --libs <library-name>
    이 명령은 지정된 라이브러리에 대해 컴파일 및 링크하는 데 필요한 컴파일러 및 링커 플래그를 출력합니다.

3. Makefile 에서 pkg-config 사용 

    LIBS = `pkg-config --libs <library-name>`
    CFLAGS = `pkg-config --cflags <library-name>`
    
    myprogram: myprogram.c
        gcc $(CFLAGS) -o myprogram myprogram.c $(LIBS)

이런 식으로 Makefile 의 LIBS, CFLAGS 등의 변수에 pkg-config를 사용해서 make를 사용해서
링커 플래그를 검색하는 과정에서 .c 파일들을 컴파일 및 링크할 때 사용할 수 있습니다.


대표적으로 C언어에서 각 파일 간의 의존성을 체크하는 방법 들 중 4가지를 체크해보았습니다.

의존성을 체크할 때 상황에 맞는 명령, 도구를 사용해서 의존성을 체크하고 프로젝트를 빌드할 수 있도록 합시다.

'코딩이야기 > C' 카테고리의 다른 글

[c] Makefile과 make에 대해서 -1-  (0) 2023.03.17

+ Recent posts