libgcc란 무엇인가: 임베디드 시스템에서의 역할과 중요성(Understanding libgcc: Its Role and Importance in Embedded Systems)

2024. 6. 11. 22:28Embedded/What is Embedded?

https://github.com/mijoungkim/making-embedded-systems/blob/main/Ch01_Intro/README.md

https://www.lisha.ufsc.br/teaching/os/exercise/hello.html

 

Hello World Program

Most of our computer science students have been through the famous "Hello World" program at least once. When compared to a typical application program ---almost always featuring a web-aware graphical user interface, "Hello World" turns into an very uninter

www.lisha.ufsc.br

대부분의 컴퓨터 공학과 학생들은 유명한 "Hello World" 프로그램을 한 번 이상 작성해 본 적이 있습니다. 일반적인 웹 기반 그래픽 사용자 인터페이스를 갖춘 응용 프로그램에 비하면, "Hello World"는 매우 단순한 코드 조각으로 보일 수 있습니다. 그러나 많은 컴퓨터 공학 학생들은 여전히 그 뒤에 숨겨진 실제 이야기를 이해하지 못한 채 지나칩니다. 이 연습의 목표는 "Hello World" 프로그램의 라이프사이클을 살펴보며 그 주제에 대해 더 깊이 이해하는 것입니다.

소스 코드 분석

우선 "Hello World"의 소스 코드부터 시작해 보겠습니다.

c코드 복사
#include <stdio.h>
int main(void)
{
    printf("Hello World!\\n");
    return 0;
}

1. 소스 코드 설명

  • 1행: **#include <stdio.h>**는 컴파일러에게 printf C 라이브러리 함수의 선언을 포함하라는 지시입니다. 이 라이브러리는 표준 입력과 출력을 처리하는 데 필요한 함수를 제공합니다.
  • 3행: 프로그램의 진입점으로 간주되는 main 함수를 선언합니다. (나중에 설명할 다른 관점이 있습니다.) 이 함수는 매개변수를 받지 않으며(이 프로그램에서는 명령줄 인수를 무시합니다), 부모 프로세스(이 경우에는 셸)에게 정수를 반환합니다. 셸은 자식 프로세스가 자신의 상태를 나타내는 8비트 숫자를 반환해야 하는 규칙을 지정합니다. 즉, 정상 종료의 경우 0, 비정상 종료의 경우 0 < n < 128, 신호로 인한 종료의 경우 **n > 128**입니다.
  • 해석1
    1. 이 함수는 매개변수를 받지 않으며(이 프로그램에서는 명령줄 인수를 무시합니다)
      • main 함수는 매개변수를 받지 않습니다.
      • 즉, 명령줄에서 추가 인수를 받아들이지 않습니다.
      • 명령줄주요 특징 및 구성 요소
        1. 프롬프트(Prompt):
          • 명령줄의 시작 부분에 표시되는 텍스트로, 사용자가 명령을 입력할 수 있도록 신호를 줍니다.
          • 예를 들어, Unix/Linux에서는 $ 또는 #, Windows에서는 **C:\\>**와 같은 형태입니다.
        2. 명령(Command):
          • 특정 작업을 수행하기 위해 입력하는 텍스트입니다.
          • 예: ls (디렉토리 목록 출력), cd (디렉토리 변경), mkdir (디렉토리 생성).
        3. 인수(Arguments):
          • 명령에 추가 정보를 제공하는 입력값입니다.
          • 예: **ls -l /home**에서 **l**과 **/home**이 인수입니다.
        4. 옵션(Options):
          • 명령의 동작을 변경하거나 세부 설정을 제공하는 추가 인수입니다.
          • 예: **ls -l**에서 **l**은 옵션입니다.
        명령줄의 예
        bash코드 복사
        $ ls -l /home
        
        
        • ls: 디렉토리 목록을 출력하는 명령
        • l: 자세한 정보와 함께 출력하는 옵션
        • /home: 명령에 제공되는 인수 (목록을 출력할 디렉토리)
        Windows
        • dir: 디렉토리 목록을 출력하는 명령
        • /p: 한 페이지씩 출력하는 옵션
        명령줄의 중요성
        1. 효율성: 많은 작업을 신속하게 수행할 수 있습니다.
        2. 자동화: 스크립트를 작성하여 반복적인 작업을 자동화할 수 있습니다.
        3. 서버 관리: GUI를 사용할 수 없는 서버 환경에서 시스템을 관리할 때 필수적입니다.
        4. 정교한 제어: 세부 설정과 고급 기능을 사용할 수 있습니다.
        명령줄 인수의 예
        bash코드 복사
        $ ./hello John
        
        
        • ./hello: 실행할 프로그램
        • John: 프로그램에 전달되는 인수
        이 경우, hello 프로그램은 **John**이라는 인수를 받아 처리할 수 있습니다.C 프로그램에서 명령줄 인수를 사용하는 방법은 다음과 같습니다:
        • argc: 명령줄 인수의 개수를 나타냅니다.
        • argv: 명령줄 인수의 배열입니다. **argv[0]**은 프로그램 이름, **argv[1]**부터는 전달된 인수입니다.
        요약
      • 명령줄은 텍스트 기반의 사용자 인터페이스로, 명령어와 인수를 입력하여 컴퓨터를 제어합니다. 효율성과 자동화, 정교한 제어 등의 이유로 중요하며, 특히 서버 환경이나 개발 작업에서 많이 사용됩니다.
      • c코드 복사 #include <stdio.h>int main(int argc, char *argv[]) { if (argc > 1) { printf("Hello, %s!\\n", argv[1]); } else { printf("Hello, World!\\n"); } return 0; }
      • C 프로그램에서 명령줄 인수 사용 예
      • 명령줄 인수는 프로그램에 특정 정보를 전달하는 데 사용됩니다. 예를 들어, hello 프로그램을 명령줄 인수로 실행할 때:
      • cmd코드 복사 C:\\> dir /p
      • Unix/Linux
      • 명령줄(Command Line)은 컴퓨터 사용자 인터페이스의 한 형태로, 텍스트 명령을 입력하여 운영 체제나 소프트웨어를 제어하는 방법을 말합니다. 명령줄 인터페이스(CLI, Command Line Interface)는 GUI(그래픽 사용자 인터페이스)와는 달리 키보드로 명령어를 입력하고 결과를 텍스트로 출력하는 방식입니다.
      • main 함수가 매개변수를 받지 않는 이유와 그 결과C 프로그램에서 main 함수는 프로그램의 진입점으로, 두 가지 형태로 정의될 수 있습니다:
        1. 매개변수를 받지 않는 형태:
        2. c코드 복사 int main(void) { // 코드 return 0; }
        3. 명령줄 인수를 받는 형태:
        4. c코드 복사 int main(int argc, char *argv[]) { // 코드 return 0; }
        main 함수가 매개변수를 받지 않는 경우이유
        • 단순성: 간단한 프로그램에서는 명령줄 인수를 받을 필요가 없습니다.
        • 학습: 프로그래밍을 처음 배우는 사람들에게 더 쉽게 이해될 수 있습니다.
        벌어지게 될 일
        • 명령줄 인수를 사용할 수 없음: 프로그램 실행 시 명령줄에서 전달되는 인수를 사용할 수 없습니다. 예를 들어, 사용자 입력이나 설정 값을 프로그램 시작 시 전달할 수 없습니다.
        • 고정된 동작: 프로그램의 동작이 고정되며, 실행 시 동적으로 변경할 수 없습니다. 모든 입력은 프로그램 내에서 직접 설정하거나, 실행 중 사용자로부터 받아야 합니다.
        예시이 프로그램은 항상 "Hello, World!"를 출력합니다. 명령줄 인수를 통해 출력할 메시지를 변경할 수 없습니다.반면에, main 함수가 매개변수를 받을 때는 프로그램이 실행 시 전달되는 정보를 처리할 수 있습니다.
        • 유연성: 사용자로부터 명령줄 인수를 받아 프로그램의 동작을 동적으로 변경할 수 있습니다.
        • 입력 처리: 파일 이름, 설정 값, 옵션 등을 명령줄 인수로 받을 수 있습니다.
        벌어지게 될 일
        • 명령줄 인수를 사용할 수 있음: 프로그램 실행 시 전달된 인수를 처리할 수 있습니다.
        • 다양한 동작: 프로그램의 동작이 동적으로 결정될 수 있습니다. 사용자가 명령줄에서 입력을 변경함으로써 다양한 결과를 얻을 수 있습니다.
        예시이 프로그램은 명령줄 인수가 제공되면 그 값을 사용해 메시지를 출력하고, 그렇지 않으면 기본 메시지를 출력합니다.
        • main 함수가 매개변수를 받지 않는 경우: 프로그램은 명령줄 인수를 사용하지 않으며, 고정된 동작을 수행합니다.
        • main 함수가 매개변수를 받는 경우: 프로그램은 명령줄 인수를 받아 유연하게 동작을 변경할 수 있습니다.
      • 요약
      • c코드 복사 #include <stdio.h>int main(int argc, char *argv[]) { if (argc > 1) { printf("Hello, %s!\\n", argv[1]); } else { printf("Hello, World!\\n"); } return 0; }
      • 이유
      • main 함수가 매개변수를 받는 경우
      • c코드 복사 #include <stdio.h>int main(void) { printf("Hello, World!\\n"); return 0; }
      • main 함수가 매개변수를 받지 않는 경우는 일반적으로 프로그램이 외부 입력을 필요로 하지 않거나, 명령줄 인수를 사용하지 않는 간단한 프로그램에 사용됩니다.
      • main 함수가 매개변수를 받지 않는 이유와 그 결과 

  1. 부모 프로세스(이 경우에는 셸)에게 정수를 반환합니다.
    • 프로그램이 종료되면, main 함수는 정수 값을 반환합니다.
    • 이 정수 값은 프로그램의 실행 결과를 나타냅니다.
    • 부모 프로세스, 즉 프로그램을 실행한 셸에게 반환됩니다.
  2. 셸은 자식 프로세스가 자신의 상태를 나타내는 8비트 숫자를 반환해야 하는 규칙을 지정합니다.
    • 셸은 자식 프로세스(프로그램)가 8비트 숫자(0부터 255 사이의 값)를 반환해야 한다고 규정합니다.
    • 이 숫자는 프로그램의 종료 상태를 나타냅니다.
  3. 즉, 정상 종료의 경우 0
    • 프로그램이 정상적으로 종료되면, main 함수는 0을 반환합니다.
    • 0은 성공적인 실행을 의미합니다.
  4. 비정상 종료의 경우 0 < n < 128
    • 프로그램이 비정상적으로 종료되면, main 함수는 1에서 127 사이의 값을 반환합니다.
    • 이 값들은 프로그램에서 발생한 다양한 오류를 나타냅니다.
  5. 신호로 인한 종료의 경우 n > 128입니다.
    • 프로그램이 외부 신호로 인해 종료되면, main 함수는 128 이상의 값을 반환합니다.
    • 예를 들어, 강제 종료 신호(SIGKILL) 등이 해당됩니다.

요약

  • main 함수는 매개변수를 받지 않고, 정수 값을 반환합니다.
  • 반환된 값은 셸이 프로그램의 실행 상태를 알 수 있도록 합니다.
  • 0은 정상 종료, 1-127은 오류로 인한 종료, 128 이상의 값은 신호로 인한 종료를 나타냅니다.

용어 및 개념 설명

  1. 매개변수의 뜻
    • 매개변수(Parameters): 함수가 호출될 때 전달되는 값이나 변수를 말합니다. 예를 들어, **int add(int a, int b)**에서 **a**와 **b**는 매개변수입니다. 함수가 수행할 작업에 필요한 입력 값을 전달하는 역할을 합니다.
  2. 추가 인수를 받지 않는다의 뜻
    • 추가 인수(Arguments): 함수를 호출할 때 실제로 전달되는 값입니다. 예를 들어, **add(5, 3)**에서 5와 3은 인수입니다.
    • main 함수에서 추가 인수를 받지 않는다는 것은 프로그램이 실행될 때 명령줄에서 전달되는 값을 사용하지 않는다는 의미입니다. 예를 들어, **int main(void)**는 추가 인수를 받지 않는 형태입니다.
  3. 셸의 뜻
    • 셸(Shell): 사용자가 컴퓨터 운영 체제와 상호작용할 수 있도록 하는 프로그램입니다. 명령줄 인터페이스(CLI)를 제공하여 명령을 입력하면, 셸이 이를 해석하고 실행합니다. 예로는 bash, zsh, Windows의 cmd 등이 있습니다.
  4. 셸은 자식 프로세스(프로그램)가 8비트 숫자(0부터 255 사이의 값)를 반환해야 한다고 규정
    • 자식 프로세스(Child Process): 셸이나 다른 프로그램이 실행하는 프로그램입니다. 예를 들어, 셸에서 **./hello_world**를 실행하면, hello_world 프로그램이 자식 프로세스가 됩니다.
    • 셸은 자식 프로세스가 실행을 마치면 종료 상태를 나타내는 값을 반환해야 한다고 규정합니다. 이 값은 8비트 숫자로, 0부터 255 사이의 값을 가집니다.
      • 0: 프로그램이 정상적으로 종료되었음을 의미합니다.
      • 1~127: 프로그램이 비정상적으로 종료되었음을 나타냅니다. 오류가 발생한 경우 주로 이 범위의 값을 반환합니다.
      • 128~255: 프로그램이 외부 신호로 인해 종료되었음을 나타냅니다. 신호는 운영 체제나 사용자가 보내는 특별한 명령입니다.
  5. 128의 의미와 127 사이의 값을 반환하는 이유
    • 127 사이의 값 반환 이유: 1에서 127 사이의 값은 프로그램 내에서 발생한 다양한 오류를 나타내는 데 사용됩니다. 이러한 값들은 오류의 종류나 원인을 구체적으로 나타낼 수 있습니다.
    • 128의 의미: 값 128 이상의 값은 프로그램이 외부 신호에 의해 종료되었음을 나타냅니다. 신호는 프로세스를 종료시키거나 다른 작업을 수행하도록 할 수 있는 운영 체제의 메커니즘입니다. 예를 들어, SIGKILL(강제 종료 신호)은 값 9에 해당하며, 이 신호로 인해 프로그램이 종료되면 반환 값은 128 + 9 = 137이 됩니다.
  6. 외부 신호로 인해 종료되면, main 함수는 128 이상의 값을 반환의 뜻
    • 프로그램이 외부 신호에 의해 종료되면, main 함수는 128 이상의 값을 반환합니다. 이는 신호의 번호를 더한 값입니다.
    • 예를 들어, SIGINT(인터럽트 신호)는 값 2에 해당합니다. 이 신호로 인해 프로그램이 종료되면 반환 값은 128 + 2 = 130이 됩니다.
    • 이 방식은 프로그램이 어떻게 종료되었는지를 운영 체제나 셸에 명확히 전달합니다.

 

요약

  • 매개변수: 함수가 입력으로 받는 값.
  • 추가 인수를 받지 않음: 명령줄에서 값을 전달받지 않음.
  • : 명령을 실행하고 해석하는 프로그램.
  • 셸이 규정한 8비트 숫자: 프로그램 종료 상태를 나타내는 값 (0~255).
  • 1~127: 프로그램 내 오류로 인한 종료.
  • 128 이상의 값: 외부 신호에 의해 프로그램이 종료됨을 나타냄.

인자(Argument)와 매개변수(Parameter)는 종종 같은 의미로 사용되지만, 엄밀히 말하면 차이점이 있습니다. 이 둘의 차이를 명확히 이해하는 것이 중요합니다.

  1. 매개변수(Parameter):
    • 함수 또는 메서드의 정의 부분에서 사용됩니다.
    • 함수가 호출될 때 전달받을 값을 지칭합니다.
    • 예를 들어, 함수 정의 **int add(int a, int b)**에서 **a**와 **b**는 매개변수입니다.
  2. 인자(Argument):
    • 함수가 호출될 때 실제로 전달되는 값입니다.
    • 예를 들어, **add(5, 3)**에서 5와 3은 인자입니다.
    • 호출 시점에 매개변수로 전달되는 실제 데이터입니다.

예시를 통한 이해

c코드 복사
// 함수 정의
int add(int a, int b) {
    return a + b;
}

// 함수 호출
int result = add(5, 3);

  • 매개변수(Parameter): **int add(int a, int b)**에서 **a**와 b.
    • 함수가 호출될 때 받을 값을 정의합니다.
  • 인자(Argument): **add(5, 3)**에서 5와 3.
    • 함수가 호출될 때 실제로 전달되는 값입니다.

요약

  • 매개변수: 함수 정의 시 사용되는 변수.
  • 인자: 함수 호출 시 전달되는 실제 값.

둘 다 함수의 입력을 나타내지만, 매개변수는 함수가 정의될 때 설정되고, 인자는 함수가 호출될 때 전달되는 값입니다.

  • 함수 정의와 호출
  • 차이점
  • 해석2차이점
    1. 매개변수(Parameter):
      • 함수 또는 메서드의 정의 부분에서 사용됩니다.
      • 함수가 호출될 때 전달받을 값을 지칭합니다.
      • 예를 들어, 함수 정의 **int add(int a, int b)**에서 **a**와 **b**는 매개변수입니다.
    2. 인자(Argument):
      • 함수가 호출될 때 실제로 전달되는 값입니다.
      • 예를 들어, **add(5, 3)**에서 5와 3은 인자입니다.
      • 호출 시점에 매개변수로 전달되는 실제 데이터입니다.
    예시를 통한 이해
    c코드 복사
    // 함수 정의
    int add(int a, int b) {
        return a + b;
    }
    
    // 함수 호출
    int result = add(5, 3);
    
    
    • 매개변수(Parameter): **int add(int a, int b)**에서 **a**와 b.
      • 함수가 호출될 때 받을 값을 정의합니다.
    • 인자(Argument): **add(5, 3)**에서 5와 3.
      • 함수가 호출될 때 실제로 전달되는 값입니다.
    요약
    • 매개변수: 함수 정의 시 사용되는 변수.
    • 인자: 함수 호출 시 전달되는 실제 값.
    둘 다 함수의 입력을 나타내지만, 매개변수는 함수가 정의될 때 설정되고, 인자는 함수가 호출될 때 전달되는 값입니다.
    매개변수와 인수는 함수 호출 과정에서 중요한 역할을 합니다. 이 두 용어는 종종 혼용되지만, 각각의 역할을 명확히 이해하는 것이 중요합니다. 호출적 관점에서 이 둘의 차이점을 설명하겠습니다.
    • 정의 시점: 함수 또는 메서드를 정의할 때 사용됩니다.
    • 목적: 함수가 호출될 때 전달받을 값을 지칭하는 변수입니다.
    • 역할: 함수 내에서 전달된 값을 받아 사용합니다.
    인수 (Argument)
    • 호출 시점: 함수 또는 메서드를 호출할 때 사용됩니다.
    • 목적: 함수가 호출될 때 매개변수에 전달되는 실제 값입니다.
    • 역할: 함수 호출 시점에 매개변수로 전달되어 함수 내에서 사용됩니다.
    예시를 통한 이해
    c코드 복사
    // 함수 정의
    int add(int a, int b) {
        return a + b;
    }
    
    // 함수 호출
    int result = add(5, 3);
    
    
    매개변수 (Parameter)
    • 함수 정의 부분에서 **int add(int a, int b)**에서 **a**와 **b**가 매개변수입니다.
    • 함수가 호출될 때 어떤 값을 받을지 지정합니다.
    인수 (Argument)
    • 함수 호출 부분에서 **add(5, 3)**에서 5와 3이 인수입니다.
    • 함수가 호출될 때 실제로 매개변수에 전달되는 값입니다.
    호출적 차이점
    1. 매개변수(Parameter):
      • 함수가 정의될 때 설정됩니다.
      • 함수가 어떤 값을 받을지 미리 정의합니다.
      • 예를 들어, **int add(int a, int b)**에서 **a**와 **b**는 함수가 받을 값의 위치와 이름을 정의합니다.
    2. 인수(Argument):
      • 함수가 호출될 때 전달됩니다.
      • 실제로 함수에 전달되는 값입니다.
      • 예를 들어, **add(5, 3)**에서 5와 3은 함수 호출 시 **a**와 **b**에 전달되는 실제 값입니다.
    요약
    • 매개변수는 함수 정의 시점에서 함수가 받을 값의 이름과 위치를 정의합니다.
    • 인수는 함수 호출 시점에서 함수에 실제로 전달되는 값입니다.
    이해를 돕기 위한 비유를 들어보면:
    • 매개변수는 편지봉투의 주소란에 해당합니다. 편지봉투에 주소를 적어 어디로 보낼지를 명시합니다.
    • 인수는 실제 편지입니다. 편지를 편지봉투에 넣어 해당 주소로 보냅니다.
  • 함수 정의와 호출
  • 매개변수 (Parameter)
  • 함수 정의와 호출
  • **인자(Argument)**와 **매개변수(Parameter)**는 종종 같은 의미로 사용되지만, 엄밀히 말하면 차이점이 있습니다. 이 둘의 차이를 명확히 이해하는 것이 중요합니다.
  • 4~8행: printf C 라이브러리 함수를 호출하여 "Hello World!\n" 문자열을 출력하고, main 함수는 상위 프로세스에 **0**을 반환합니다.

이처럼 간단해 보이지만, 여기에는 중요한 개념들이 숨어 있습니다.

라이프사이클과 숨겨진 이야기

"Hello World" 프로그램의 라이프사이클을 이해하기 위해서는 프로그램이 작성되고 실행되는 과정을 단계별로 살펴볼 필요가 있습니다:

  1. 소스 코드 작성: 위와 같은 C 소스 코드를 작성합니다.
  2. 컴파일 단계:
    • 전처리: **#include <stdio.h>**와 같은 지시문을 처리하여 필요한 헤더 파일의 내용을 소스 코드에 포함시킵니다.
    • 컴파일: 전처리된 소스 코드를 기계어로 변환하여 오브젝트 파일을 생성합니다.
    • 링크: 오브젝트 파일을 필요한 라이브러리와 결합하여 실행 가능한 프로그램을 만듭니다.
  3. 실행 단계:
    • 로딩: 실행 파일이 메모리에 로드됩니다.
    • 실행: 운영 체제가 프로그램을 실행하고, main 함수가 호출됩니다.
    • 종료: 프로그램이 종료되면 main 함수는 **return 0;**을 통해 셸에 정상 종료를 알립니다.

이 모든 과정에서, 간단한 "Hello World" 프로그램도 컴파일러, 링커, 운영 체제 등 여러 요소들이 협력하여 작동하게 됩니다. 이를 통해 학생들은 프로그램의 기본 구조와 실행 흐름을 이해할 수 있습니다.

요약

"Hello World" 프로그램은 단순해 보이지만, 그 뒤에는 컴퓨터 시스템의 중요한 개념들이 숨어 있습니다. 이 프로그램을 통해 컴파일 과정, 프로그램의 진입점과 종료, 그리고 운영 체제와의 상호작용을 이해하는 것이 중요합니다.


이제 "Hello World"의 컴파일 프로세스를 살펴보겠습니다. 다음 토론에서는 널리 사용되는 GNU 컴파일러( gcc )와 관련 도구( binutils )를 다루겠습니다. 우리는 다음과 같이 프로그램을 컴파일할 수 있습니다:

sh코드 복사
# gcc -Os -c hello.c

그러면 hello.o 객체 파일이 생성됩니다. 더 구체적으로 살펴보면,

sh코드 복사
# file hello.o

plaintext코드 복사
hello.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

**hello.o**는 IA-32 아키텍처용으로 컴파일된 재배치 가능한 객체 파일이며(이 연구에서는 표준 PC를 사용했습니다), ELF(Executable and Linking Format)에 저장되어 있으며 기호 테이블(제거되지 않음)이 포함되어 있습니다.

또한,

sh코드 복사
# objdump -hrt hello.o

plaintext코드 복사
hello.o: file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
 0 .text         00000011  00000000  00000000  00000034  2**2
                 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
 1 .data         00000000  00000000  00000000  00000048  2**2
                 CONTENTS, ALLOC, LOAD, DATA
 2 .bss          00000000  00000000  00000000  00000048  2**2
                 ALLOC
 3 .rodata.str1.1 0000000d 00000000  00000000  00000048  2**0
                 CONTENTS, ALLOC, LOAD, READONLY, DATA
 4 .comment      00000033  00000000  00000000  00000055  2**0
                 CONTENTS, READONLY

Symbol table:
00000000 l    d  .text 00000000 .text
00000000 l    d  .data 00000000 .data
00000000 l    d  .bss  00000000 .bss
00000000 l    d  .rodata.str1.1 00000000 .rodata.str1.1
00000000 l    d  .comment 00000000 .comment
00000000 g    F  .text 00000011 main
00000000       *UND*  00000000

여기서 **hello.o**에는 5개의 섹션이 있습니다:

  1. .text: "Hello World" 프로그램의 실제 코드가 포함되어 있습니다. 이는 프로그램 로더가 프로세스의 코드 세그먼트를 초기화하는 데 사용됩니다.
  2. .data: 초기화된 전역 변수나 정적 지역 변수가 여기에 저장됩니다. "Hello World"에는 해당 변수가 없으므로 비어 있습니다.
  3. .bss: 초기화되지 않은 전역 변수나 정적 지역 변수가 여기에 저장됩니다. "Hello World"에는 해당 변수가 없으므로 이 섹션도 비어 있습니다.
  4. .rodata: 읽기 전용 데이터가 포함되어 있습니다. "Hello World!\n" 문자열이 이 섹션에 저장됩니다.
  5. .comment: 컴파일러에 의해 추가된 주석 정보가 포함되어 있습니다.

기호 테이블에는 주소 **00000000**에 바인딩된 main 심볼과 정의되지 않은 심볼이 있습니다. 또한 재배치 테이블은 .text 섹션에서 외부 섹션에 대한 참조를 재배치하는 방법을 알려줍니다. 첫 번째 재배치 가능 기호는 .rodata 섹션에 포함된 "Hello World!\n" 문자열에 해당합니다. 두 번째 재배치 가능 기호는 puts 함수로, printf 호출의 결과로 생성된 libc 함수입니다.

어셈블리 코드를 보면 더 잘 이해할 수 있습니다:

sh코드 복사
# gcc -Os -S hello.c -o -

assembly코드 복사
        .file   "hello.c"
        .section .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "Hello World!"
        .text
        .align 2
.globl main
        .type   main,@function
main:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   $.LC0
        call    puts
        xorl    %eax, %eax
        leave
        ret
.Lfe1:
        .size   main,.Lfe1-main
        .ident  "GCC: (GNU) 3.2 20020903 (Red Hat Linux 8.0 3.2-7)"

어셈블리 코드를 보면 ELF 섹션 플래그의 출처가 명확해집니다. 예를 들어, 섹션 **.text**는 32비트로 정렬됩니다. 또한 .comment 섹션의 출처를 보여줍니다. 단일 문자열을 출력하기 위해 printf 대신 **puts**가 사용되었습니다. 이는 최적화 옵션 **-Os**의 결과입니다.

요약

이 컴파일 프로세스는 소스 코드를 객체 파일로 변환하고, 각 섹션과 기호 테이블을 생성하여 프로그램이 실행될 준비를 하는 과정을 설명합니다. 각 섹션의 역할과 컴파일러의 최적화 동작에 대해 이해할 수 있습니다.


 

이제 hello.o 객체 파일을 실행 파일로 변환하는 과정을 살펴보겠습니다. 보통 다음과 같은 명령을 사용할 수 있다고 생각할 수 있습니다:

sh코드 복사
# ld -o hello hello.o -lc

그러나, 이 명령을 실행하면 다음과 같은 경고 메시지가 나타납니다:

plaintext코드 복사
ld: warning: cannot find entry symbol _start; defaulting to 08048184

경고 메시지의 의미

이 경고 메시지는 링커가 프로그램의 진입점(entry point)인 _start 심볼을 찾을 수 없다는 것을 의미합니다. 여기서 **_start**는 실제로 프로그램이 실행을 시작하는 지점입니다.

프로그래머 관점 vs 실제 실행

프로그래머의 관점에서는 main 함수가 프로그램의 진입점처럼 보입니다. 그러나 실제로는 main 함수가 호출되기 전에 많은 초기화 코드가 실행됩니다. 이 초기화 코드는 보통 컴파일러와 운영 체제에서 제공됩니다.

올바른 링크 명령

프로그램의 진입점을 올바르게 지정하기 위해서는, 다음과 같이 보다 구체적인 명령을 사용해야 합니다:

sh코드 복사
# ld -static -o hello -L`gcc -print-file-name=` /usr/lib/crt1.o /usr/lib/crti.o hello.o /usr/lib/crtn.o -lc -lgcc

이 명령이 정상적으로 작동하지 않으면, 시스템의 라이브러리 경로를 확인하고 필요한 파일들이 있는지 점검해야 합니다.

정적 링크의 이유

이 명령에서는 정적 링크를 사용합니다. 정적 링크는 다음과 같은 이유로 사용됩니다:

  1. 동적 라이브러리의 복잡성 회피: 동적 라이브러리가 어떻게 작동하는지 논의하지 않기 위해.
  2. 불필요한 코드 확인: 라이브러리(특히 libclibgcc) 구현 방식으로 인해 "Hello World" 프로그램에 얼마나 많은 불필요한 코드가 포함되는지 보여주기 위해.

파일 크기 비교

정적 링크를 통해 생성된 실행 파일의 크기를 확인해보겠습니다:

sh코드 복사
# ls -l hello.c hello.o hello

plaintext코드 복사
-rw-r--r-- 1 user user    84 hello.c
-rw-r--r-- 1 user user   788 hello.o
-rwxr-xr-x 1 user user 445506 hello

실행 파일 내용 분석

다음 명령을 통해 실행 파일에 링크된 내용을 자세히 살펴볼 수 있습니다:

sh코드 복사
# nm hello
# objdump -d hello

이 명령들은 실행 파일의 심볼 테이블과 디스어셈블리 코드를 확인할 수 있게 해줍니다.

동적 연결에 대한 추가 정보

동적 연결에 대한 자세한 내용은 "Program Library HOWTO" 문서를 참조하십시오.


프로그램 로드 및 실행 과정

POSIX 운영 체제에서 프로그램을 로드하고 실행하는 과정은 주로 두 개의 시스템 호출, **fork**와 **execve**를 사용합니다.

  1. fork 시스템 호출:
    • 부모 프로세스가 자신을 복제하여 새로운 자식 프로세스를 생성합니다.
    • 자식 프로세스는 부모 프로세스의 메모리 공간과 거의 동일한 복사본을 가집니다.
  2. execve 시스템 호출:
    • 자식 프로세스가 **execve**를 호출하여 특정 프로그램을 메모리에 로드하고 실행을 시작합니다.
    • 이 과정에서 자식 프로세스의 기존 메모리 공간은 새로운 프로그램으로 대체됩니다.

예시

쉘에서 외부 명령을 입력할 때마다 이 과정이 수행됩니다. 이를 확인하기 위해 **strace**를 사용하여 시스템 호출을 추적할 수 있습니다.

sh코드 복사
# strace -i ./hello > /dev/null

출력 예시:

plaintext코드 복사
[????????] execve("./hello", ["hello"], [/* 46 vars */]) = 0
...
[08053d44] write(1, "Hello World!\\n", 13) = 13
...
[0804e7ad] _exit(0) = ?

이 출력은 execve 시스템 호출과 puts 함수 호출, 그리고 main 함수에서 반환된 값을 사용하여 종료하는 호출을 보여줍니다.

ELF 실행 파일 분석

**execve**가 수행하는 로딩 절차의 세부 사항을 이해하기 위해 ELF(Executable and Linking Format) 실행 파일을 살펴보겠습니다.

sh코드 복사
# readelf -l hello

출력 예시:

plaintext코드 복사
Elf file type is EXEC (Executable file)
Entry point 0x80480e0
There are 3 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0x55dac 0x55dac  R E 0x1000
  LOAD           0x055dc0 0x0809edc0 0x0809edc0 0x01df4 0x03240  RW 0x1000
  NOTE           0x000094 0x08048094 0x08048094 0x00020 0x00020  R  0x4

 Section to Segment mapping:
  Segment Sections...
   00     .init .text .fini .rodata __libc_atexit __libc_subfreeres .note.ABI-tag
   01     .data .eh_frame .got .bss
   02     .note.ABI-tag

이 출력에는 hello 실행 파일의 전체 구조가 표시됩니다. 중요한 부분을 살펴보면:

  1. 첫 번째 프로그램 헤더:
    • 프로세스의 코드 세그먼트에 해당합니다.
    • 파일 오프셋 0x000000에서 시작하여 프로세스 주소 공간의 0x08048000에 매핑됩니다.
    • 크기는 0x55dac 바이트이며, 읽기 전용(R) 및 실행 가능(E) 플래그가 지정되어 있습니다.
  2. 두 번째 프로그램 헤더:
    • 프로세스의 데이터 세그먼트에 해당합니다.
    • 파일 오프셋 0x055dc0에서 시작하여 프로세스 주소 공간의 0x0809edc0에 매핑됩니다.
    • 파일 크기는 0x01df4 바이트이며, 메모리 크기는 0x03240 바이트입니다.
    • 읽기 및 쓰기 가능(RW) 플래그가 지정되어 있습니다.
  3. 세 번째 프로그램 헤더:
    • ELF 파일의 메타데이터에 해당합니다. 이 논의에서는 중요하지 않습니다.

메모리 맵 확인

실행 중인 프로그램의 메모리 맵을 확인하기 위해 proc 파일 시스템을 사용할 수 있습니다. hello 프로그램이 충분히 오래 실행되는 동안 다음 명령을 사용합니다:

sh코드 복사
# cat /proc/$(pgrep hello)/maps

출력 예시:

plaintext코드 복사
08048000-0809e000 r-xp 00000000 03:06 479202 .../hello
0809e000-080a1000 rw-p 00055000 03:06 479202 .../hello
080a1000-080a3000 rwxp 00000000 00:00 0
bffff000-c0000000 rwxp 00000000 00:00 0

  1. 첫 번째 매핑된 영역:
    • 프로세스의 코드 세그먼트에 해당합니다.
  2. 두 번째 및 세 번째 매핑된 영역:
    • 데이터 세그먼트 (data, bss, heap)에 해당합니다.
  3. 네 번째 영역:
    • 스택에 해당합니다. 이는 ELF 파일에 명시되지 않지만, 실행 중인 프로세스의 메모리 공간에 할당됩니다.

요약

  • 로드 및 실행 과정: **fork**와 execve 시스템 호출을 통해 부모 프로세스가 자식 프로세스를 생성하고, 자식 프로세스가 새로운 프로그램을 메모리에 로드하고 실행합니다.
  • ELF 분석: ELF 파일의 프로그램 헤더를 통해 코드 세그먼트와 데이터 세그먼트가 어떻게 메모리에 매핑되는지 확인할 수 있습니다.
  • 메모리 맵: 실행 중인 프로세스의 메모리 맵을 통해 코드, 데이터, 스택 세그먼트를 확인할 수 있습니다.

프로그램 종료 과정

"Hello World" 프로그램이 main 함수의 return 문을 실행하면, 프로그램은 시스템 호출을 통해 종료됩니다. 이 과정에서 중요한 부분들을 살펴보겠습니다.

종료 과정의 주요 단계

  1. 주 함수의 반환:
    • main 함수에서 **return 0;**이 실행되면, 반환 값 **0**이 주변 함수로 전달됩니다. 이 함수는 프로그램이 정상적으로 종료되었음을 의미하는 값을 시스템에 전달합니다.
  2. exit 시스템 호출:
    • 주변 함수 중 하나는 exit 시스템 호출을 사용하여 반환 값을 운영 체제에 전달합니다.
    • exit 시스템 호출은 전달된 값을 상위 프로세스(부모 프로세스)에게 전달하고, 리소스를 시스템에 반환하며, 깨끗한 프로세스 종료를 수행합니다.
  3. 상위 프로세스와의 상호작용:
    • 부모 프로세스는 자식 프로세스가 종료될 때까지 기다립니다.
    • 이 과정에서 wait 시스템 호출을 사용하여 자식 프로세스의 종료 상태를 확인합니다.

strace를 통한 종료 과정 추적

strace 도구를 사용하여 종료 과정을 추적할 수 있습니다. 다음 명령은 쉘을 통해 hello 프로그램을 실행하고, 종료 상태를 확인하는 과정을 보여줍니다.

sh코드 복사
# strace -e trace=process -f sh -c "hello; echo $?" > /dev/null

출력 예시:

plaintext코드 복사
execve("/bin/sh", ["sh", "-c", "hello; echo 0"], [/* 46 vars */]) = 0
fork() = 8321
[pid 8320] wait4(-1, <unfinished ...>
[pid 8321] execve("./hello", ["hello"], [/* 46 vars */]) = 0
[pid 8321] _exit(0) = ?
<... wait4 resumed> [WIFEXITED(s) && WEXITSTATUS(s) == 0], 0, NULL) = 8321
--- SIGCHLD (Child exited) ---
wait4(-1, 0xbffff06c, WNOHANG, NULL) = -1 ECHILD (No child processes)
_exit(0)

세부 설명

  1. 쉘 실행:
    • execve("/bin/sh", ["sh", "-c", "hello; echo 0"], [/* 46 vars */]) = 0: 쉘이 실행되고, 명령 **hello; echo $?**를 실행합니다.
  2. 자식 프로세스 생성:
    • fork() = 8321: 부모 프로세스(쉘)가 자식 프로세스를 생성합니다.
  3. 자식 프로세스의 실행:
    • [pid 8320] wait4(-1, <unfinished ...>: 부모 프로세스가 자식 프로세스의 종료를 기다립니다.
    • [pid 8321] execve("./hello", ["hello"], [/* 46 vars */]) = 0: 자식 프로세스가 hello 프로그램을 실행합니다.
    • [pid 8321] _exit(0) = ?: hello 프로그램이 정상적으로 종료되며, _exit 시스템 호출을 통해 반환 값 **0**을 전달합니다.
  4. 부모 프로세스가 자식 프로세스의 종료를 확인:
    • <... wait4 resumed> [WIFEXITED(s) && WEXITSTATUS(s) == 0], 0, NULL) = 8321: 부모 프로세스가 자식 프로세스의 종료 상태를 확인합니다.
    • -- SIGCHLD (Child exited) ---: 자식 프로세스가 종료되었음을 나타내는 신호(SIGCHLD)를 받습니다.
    • wait4(-1, 0xbffff06c, WNOHANG, NULL) = -1 ECHILD (No child processes): 더 이상 기다릴 자식 프로세스가 없음을 확인합니다.
  5. 부모 프로세스의 종료:
    • _exit(0): 부모 프로세스도 정상적으로 종료됩니다.

요약

  • main 함수의 return 문은 종료 값을 주변 함수에 전달합니다.
  • exit 시스템 호출을 통해 종료 값이 상위 프로세스에 전달되고, 리소스가 반환되며, 깨끗한 종료가 수행됩니다.
  • strace 도구를 사용하여 이 과정을 추적할 수 있습니다.

결론 및 문제 해결

이 연습의 목적은 Java 애플릿이나 간단한 "Hello World" 프로그램 뒤에 많은 시스템 소프트웨어가 있다는 사실을 컴퓨터 과학을 처음 배우는 학생들에게 이해시키는 것입니다. 이러한 프로그램이 마술처럼 실행되는 것이 아니라, 복잡한 시스템 호출과 다양한 운영 체제 구성 요소가 협력하여 실행된다는 점을 강조합니다.


문제 해결

[1] /usr/lib/crt1.o를 찾을 수 없음

명령을 실행할 때 다음과 같은 오류가 발생할 수 있습니다:

sh코드 복사
$ ld -static -o hello -L`gcc -print-file-name=` /usr/lib/crt1.o /usr/lib/crti.o hello.o /usr/lib/crtn.o -lc -lgcc

오류 메시지:

plaintext코드 복사
ld: /usr/lib/crt1.o를 찾을 수 없음: 해당 파일 또는 디렉터리가 없음
ld: /usr/lib/crti.o를 찾을 수 없음: 해당 파일 또는 디렉터리가 없음
ld: /usr/lib/crtn.o를 찾을 수 없음: 해당 파일 또는 디렉터리가 없음

이 파일들은 시스템의 다른 디렉토리에 있을 수 있습니다. 다음 명령을 사용하여 파일을 찾을 수 있습니다:

sh코드 복사
$ find /usr/lib -name 'crt1.o'

예시 출력:

plaintext코드 복사
/usr/lib/x86_64-linux-gnu/crt1.o

위 명령으로 파일을 찾을 수 없다면 libc6-dev 패키지를 설치한 후 다시 시도해야 합니다. 64비트 Ubuntu 14.04 시스템의 경우:

sh코드 복사
$ sudo apt-get install libc6-dev

그런 다음 ld 명령을 올바른 디렉토리에 적용합니다. 예를 들어:

sh코드 복사
$ ld -static -o hello -L`gcc -print-file-name=` /usr/lib/x86_64-linux-gnu/crt1.o hello.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/crtn.o -lc -lgcc

[2] `_Unwind_Resume'에 대한 정의되지 않은 참조

최신 버전의 GCC에서는 다음과 같이 정의되지 않은 참조가 발생할 수 있습니다:

plaintext코드 복사
//usr/lib/x86_64-linux-gnu/libc.a(iofclose.o): In function `_IO_new_fclose':
(.text+0x20c): undefined reference to `_Unwind_Resume'
//usr/lib/x86_64-linux-gnu/libc.a(iofclose.o):(.eh_frame+0x1f3): undefined reference to `__gcc_personality_v0'
//usr/lib/x86_64-linux-gnu/libc.a(iofflush.o): In function `_IO_fflush':
(...)

GCC의 예외 지원 라이브러리(-lgcc_eh)를 포함해야 하지만, 이는 순환 종속성을 발생시킬 수 있습니다. 다음 명령을 사용하여 문제를 해결합니다:

sh코드 복사
$ ld -static -o hello -L`gcc -print-file-name=` /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/crtn.o --start-group -lc -lgcc -lgcc_eh --end-group

  • -start-group-end-group 플래그는 순환 종속성을 해결하는 데 사용됩니다. 더 자세한 내용은 ld 매뉴얼 페이지를 참조하십시오:
sh코드 복사
$ man ld


요약

  • Java 애플릿이나 간단한 "Hello World" 프로그램 뒤에도 많은 시스템 소프트웨어가 있으며, 이를 통해 프로그램이 실행됩니다.
  • 시스템 호출과 운영 체제 구성 요소의 역할을 이해하는 것이 중요합니다.
  • 컴파일 및 링크 과정에서 발생할 수 있는 문제들을 해결하는 방법을 배웠습니다.

자주 묻는 질문 (FAQ)

"libgcc"란 무엇입니까? 왜 연계에 포함되나요?

libgcc는 컴파일러 내부 라이브러리로, 컴파일러가 특정 언어 기능을 구현하는 데 사용됩니다. 이 라이브러리는 특히 대상 아키텍처에서 직접 지원되지 않는 기능들을 구현하는 역할을 합니다.

주요 역할 및 기능

  1. 언어 구성 요소의 구현:
    • C 언어의 특정 연산자나 기능이 대상 아키텍처에서 단일 어셈블리 명령으로 구현되지 않는 경우가 있습니다. 예를 들어, C의 모듈 연산자(%)는 모든 아키텍처에서 직접 지원되지 않을 수 있습니다.
    • 이러한 경우, 컴파일러는 해당 연산을 수행하기 위해 libgcc 라이브러리의 함수를 호출합니다.
  2. 기본 연산 지원:
    • 나눗셈, 곱셈, 문자열 조작(예: 메모리 복사) 등의 기본 연산은 종종 **libgcc**에서 구현됩니다. 이는 특히 메모리가 제한된 시스템(예: 마이크로컨트롤러)에서 인라인 코드를 생성하는 대신 함수 호출로 구현하는 것이 더 효율적일 수 있기 때문입니다.
  3. 예외 처리 지원:
    • **libgcc**는 또한 C++의 예외 처리와 같은 고급 기능을 지원합니다. 예외 처리 루틴은 일반적으로 복잡하며, 이러한 루틴을 라이브러리로 분리하여 구현하는 것이 더 효율적입니다.

연계 시 포함 이유

  1. 호환성 및 이식성:
    • 컴파일러가 생성한 코드가 다양한 아키텍처에서 제대로 동작하도록 보장하기 위해, **libgcc**는 필수적입니다.
    • 이는 특히 다양한 하드웨어 플랫폼을 지원해야 하는 임베디드 시스템 개발에서 중요합니다.
  2. 코드 크기 및 성능 최적화:
    • 제한된 메모리 리소스를 사용하는 시스템에서, 인라인 코드 대신 라이브러리 함수를 호출하면 코드 크기를 줄일 수 있습니다.
    • 함수 호출을 통해 필요한 기능만 메모리에 로드되므로 성능 최적화에도 기여할 수 있습니다.

요약

libgcc는 컴파일러가 특정 언어 기능을 구현하는 데 사용하는 내부 라이브러리로, 특히 대상 아키텍처에서 직접 지원되지 않는 기능들을 구현하는 역할을 합니다. 나눗셈, 곱셈, 문자열 조작 등 기본 연산을 지원하며, 예외 처리와 같은 고급 기능도 제공합니다. 이를 통해 코드의 호환성, 이식성, 최적화가 가능해집니다.