gprof로 프로파일 정보 확인

: 프로파일 정보는 바이너리가 수행될 때 생성되는 정보로, 어떤 함수가 몇 번 호출되었고 이 함수의 수행 시간은 얼마나 걸렸는지 등을 알려준다. 이러한 프로파일 정보를 보기 위해서는 컴파일 시 -pg 옵션을 사용해야하고, 이는 실행 바이너리 파일에 프로파일 정보를 출력하는 코드를 집어넣고 실행파일을 실행한 후에 gmon.out이라는 파일을 생성한다. grpof는 이 gmon.out이라는 파일을 이용해 프로파일 정보를 얻을 수 있다.

// main.c
#include <stdio.h>

void func1()
{
    printf("Hello World \n");
}

void func2(int delay)
{
    printf("Delay: %d", delay);

    while (delay--);
}

int main()
{
    for (int i = 0; i < 10; ++i) {
        func1();
    }

    for (int i = 0; i < 100; ++i) {
        func2(i);
    }

    return 0;
}

위와 같은 소스가 있다고 하자. gprof를 통해 프로파일 정보를 알아보는 절차는 다음과 같다.

  1. gcc -pg -g -o main main.c: 명령으로 컴파일을 수행하고 main 명령을 몇 차례 수행함으로써 프로그램을 실행한다.
  2. gprof -b ./main > main.profile: 이 명령으로 main.profile 파일에 프로파일 정보를 저장한다. -b 옵션은 각 필드에 대한 자세한 설명을 제거하는 옵션이다.

'빌드&테스트도구 > 기타' 카테고리의 다른 글

readelf - ELF파일 정보 보기  (0) 2020.09.23
nm - 심볼 테이블 확인  (0) 2020.09.22
Mangling - 심볼명 확인  (0) 2020.09.20

c++filt 명령으로 맹글링되기 전의 C++/JAVA 심볼명 보기

NOTE
맹글링(mangling)이란 C++, JAVA와 같은 객체지향 언어에서 함수명을 클래스명과 인자 타입으로 변경하는 것을 말한다.
class Config {
public : 
    int OptionCheck(char *flag);
    int LineParse(char *line);
};

위와 같은 클래스가 존재할 때 각각의 멤버 메소드는

Config::OptionCheck(char*)  ==>  _ZN6Config11OptionCheckEPc
Config::LineParse(char*)  ==>  _ZN6Config9LineParseEPc

와 같이 클래스명, 함수명, 인자 타입을 이용하여 컴파일러 내부적으로 심볼명이 변경된다. 이러한 데에는 메소드 오버로딩, 가상함수 등을 지원하기 위한 목적을 가지고 있다.

이를 위해 맹글링된 심볼명, 맹글링 된 심볼명을 이용해 원본(디맹글링된) 함수명을 알고싶으면 다음과 같이 사용하면 된다.

  • c++filt {de-mangling symbold_name}: 맹글링 된 심볼명을 이용해 디맹글링 함수명을 얻는다.
  • nm {binary_file} | grep _ZN: 바이너리 파일의 맹글링 된 심볼명들을 확인한다.

'빌드&테스트도구 > 기타' 카테고리의 다른 글

readelf - ELF파일 정보 보기  (0) 2020.09.23
nm - 심볼 테이블 확인  (0) 2020.09.22
gprof - 프로파일 정보 확인  (0) 2020.09.21

코어파일을 이용한 디버깅

코어파일이란 프로세스에 예외가 발생해 중지되는 순간의 프로세스 수행 이미지를 말한다. 이러한 코어 파일에는 메모리, 레지스터, 스택 등의 정보들로 구성된다. 이러한 코어 파일에는 애초에 디버깅 정보를 메모리에 로드하지 않았으므로 디버깅을 위한 정보가 들어가 있지 않다. 따라서 코어 파일 디버깅을 위해서는 코어파일과 함께 디버깅 정보가 들어있는 프로그램 실행 파일(main)을 함께 사용해야 한다.


코어 파일이 생성될 수 있는 예외 상황으로는 다음과 같은 것들이 있다.

  1. 키보드 종료 신호
  2. 잘못된 명령어 수행
  3. 디버깅을 위한 브레이크 포인트
  4. 비정상 오류
  5. 잘못된 메모리 참조
  6. CPU 시간 제한 초과
  7. 파일 크기 제한 초과
  8. 잘못된 시스템 콜

이러한 예외 상황이 발생할 경우 코어 파일은 자동으로 생기나 만약 생기지 않는다면 쉘에서 코어 파일의 크기가 0으로 설정되어 있는 경우이므로 이 때, 다음 명령어를 통해 코어 파일이 생길 수 있도록 해 두자.

$ unlimit -S -c 100000000


생성된 코어파일을 이용해 gdb를 수행하는 방법은 다음과 같다.

gdb {program_name} {core_file_name} : gdb main core.1408


이렇게 gdb를 실행하면 코어 파일에 있는 정보로 무엇 때문에 프로그램이 이상 종료했는지 출력하고, 심볼을 로드하고 어떤 함수에 문제가 있었는지 출력해 준다. 다음으로는 bt를 통해 스택프레임 정보를 확인하고 각종 명령어들을 사용하며 디버깅하면 된다.


실행중인 프로세스 디버깅

프로그램이 가끔 알지 못하는 이유로 오동작을 발생시킨다면 코어 파일도 없고 디버깅하기에 상당히 어려움을 줄 것이다. 이러한 경우에 gdb를 이용해 s 명령을 통해 계속 반복적으로 동작시키는 것은 상당한 시간 낭비일 것이다.

이러한 경우들에는 그냥 프로그램을 수행 시키고 오동작이 발생한 시점에 attach 명령으로 해당 프로그램에 gdb를 붙여 디버깅을 하면 된다.

사용되는 명령어는 다음과 같다.

$ attach {PID}

'빌드&테스트도구 > gdb' 카테고리의 다른 글

gdb(1) - gdb 기본 사용방법  (1) 2020.09.14

gdb란?

  • gdb는 GNU에서 만든 디버거이다.
  • gdb는 C, C++, Objective-C, Java, Go, Rust 등의 많은 언어를 지원하며, arm, x86, IA-64 등 수 많은 아키텍쳐를 지원한다.

gdb를 이용한 간단한 디버깅

아래와 같은 코드가 있다고 가정하자.

// main.c
#include <stdio.h>

~

위 코드를 다음과 같은 명령어로 컴파일하자.

gcc -g -o main main.c

  • gdb를 이용해 컴파일하려면 -g 옵션을 붙여야 한다. 이는 컴파일 시 실행파일에 여러 디버깅 정보(심볼 문자열, 심볼 주소, 컴파일에 사용된 소스파일, 인스트럭션에 해당하는 코드 정보 등)를 삽입하는 옵션이다.
  • -g 옵션을 주고 컴파일 시 최적화 옵션(-O 등)을 사용하지 않는 것이 좋다. 이는 코드 및 어셈블리 코드에 변형을 가하기 때문에 디버깅이 힘들어질 수 있다.

이 상태로 ./main 명령어를 이용해 실행한다면 오류가 발생하는 것을 확인할 수 있다.

실행 및 종료

gdb를 이용해 프로그램을 load하는 방법은 아래와 같다.

  • gdb {program_name} : gdb main
  • gdb {program_name} {core_file_name} : gdb main core.1408
  • gdb {program_name} {실행중인 PID} : gdb main 1408

gdb를 종료하는 방법은 아래와 같다.

  • q
  • [Ctrl] + d

gdb를 이용해 프로그램을 load한 상태에서 프로그램을 실행시키는 방법은 다음과 같다.

  • r
  • r arg1 arg2 : arg1과 arg2를 프로그램 인자로 실행
  • k : 프로그램 실행 종료

현재 지점에서 라인 단위, 또는 다음 브레이크 포인트까지 진행하는 명령어는 다음과 같다.

명령어 설명
s (step) 현재 행 수행후 멈춤.
함수호출 지점에서는 함수 내부로 들어감  
s 6 s를 6번 수행한 것과 같음
n (next) 현재 행 수행후 멈춤
함수 호출 지점에서는 함수 내부로 들어가지 않고 수행 후 다음 행으로 감  
n 6 n을 6번 수행한 것과 같음
c (continue) 다음 브레이크 포인트를 만날 때 까지 수행
u (until) 현재 loop문에서 빠져나올 때 까지 수행
for  
finish 현재 함수를 수행하고 빠져나감
return 현재 함수를 수행하지 않고 빠져 나감
return 1 현재 함수를 수행하지 않고 빠져 나감
return 값으로 1을 줌  
advance 20 현재 파일의 20번 라인을 만날 때 까지 진행
advance file.c:20 file.c 파일의 20번 라인을 만날 때 까지 진행

소스 보기

gdb를 이용해 프로그램을 load한 뒤 소스 코드를 보는 방법은 다음과 같다.

명령어 설명
l main 함수를 기점으로 소스 출력
l 10 Line number 10 을 기준으로 출력
l func func 함수의 소스를 출력
l - 출력된 행의 이전 행을 출력
l file.c:func file.c파일의 func 함수 부분을 출력
l file.c:10 file.c의 Line number 10 부근을 출력
set listsize 20 l 명령의 기본 출력 코드수를 20라인으로 설정

브레이크 포인트

프로그램 수행 시 특정 지점 또는 특정 조건에서 break하고자 하는 경우가 있다. 이 때 다음 명령어를 사용하면 된다.

명령어 설명
b func func 심볼의 시작 부분(함수 내부 진입 전)에 브레이크 포인트 설정
b 10 10번 라인에 설정
b file.c:func file.c 파일의 func 심볼에 설정
b file.c:10 file.c 파일의 10번 라인에 설정
b +2 현재 라인에서 2개 라인 이후 지점에 설정
b -2 현재 라인에서 2개 라인 이전 지점에 설정
b *0x8040000 0x8040000 주소에 설정(어셈블리 디버깅 시 사용)
b 10 if myVar == 0 10번 라인에서 myVar의 값이 0 일때 브레이크
rb func* func*에 해당하는 모든 심볼에 설정
rb ^fun fun으로 시작하는 모든 심볼에 설정
rb TestClass:: TestClass에 해당하는 모든 심볼에 설정

브레이크 포인트를 설정하게 되면 브레이크 포인트마다 고유한 숫자(1부터 시작)가 부여된다. 이 때, 해당 브레이크 포인트에 추가적인 조건을 걸고 싶을 때 다음과 같은 명령어를 사용하면 된다.

  • condition {brakpoint_ID} myVar == 0
  • condition {brakpoint_ID} myFunc(0) > 0 : myFunc(0)의 결과값이 0보다 클 때 브레이크
  • ignore {brakpoint_ID} 100 : 해당 브레이크 포인트가 100번 지나갈 때 까지 무시. 이는 loop문에서 유용하다.

이러한 브레이크 포인트는 gdb가 종료될 때 까지 유효하다. 따라서 한 번만 사용하는 브레이크 포인트는 b 명령어 대신 tb 명령어를 사용하면 된다.

브레이크 포인트 제거

명령어 설명
cl func func 함수의 시작 부분에 브레이크 포인트 지움
cl 10 10번 라인의 브레이크 포인트 지움
d 모든 브레이크 포인트를 지움
disable br 모든 브레이크 포인트 비활성화
disable br 1 3 1번, 3번 브레이크 포인트 비활성화
enable br 모든 브레이크 포인트 활성화
enable br 1 1번 브레이크 포인트 활성화
enable br once 1 1번 브레이크 포인트를 활성화하고, 한 번만 걸리고 비활성화되게 설정
enable br delete 1 1번 브레이크 포인트를 활성화 하고, 한 번ㅁ나 걸리고 제거되게 설정

현재 설정되어있는 브레이크 포인트를 보려면 다음의 명령어를 사용하면 된다.

  • info breakpoints, info b

와치 포인트 설정

특정 변수의 값이 바뀔 때 마다 브레이크를 걸려면 와치 포인트를 사용한다. 이 때, 지역변수에 대한 설정은 해당 지역 변수의 스코프 내에서 설정하여야 한다.
이는 다음과 같이 사용할 수 있다.

  • watch {myVariable} : myVariable에 값이 쓰여질 때 마다 브레이크
  • rwatch {myVariable} : myVariable의 값이 읽혀질 때 마다 브레이크
  • awatch {myVariable} : myVariable에 값이 쓰여지거나 읽혀질 때 마다 브레이크

변수 및 레지스터의 값 검사

명령어 설명
info locals 현재 스코프에서 모든 지역변수의 값들을 확인
info variables 모든 전역변수의 값들을 확인
Non-debugging 심볼들도 포함되어 출력됨.  
info (all-)registers 모든 레지스터(MMX 레지스터 포함)의 값을 확인한다.
p myVariable myVariable의 값을 출력
p *ptr  
p **ptr 포인터 변수의 경우 주소값이 아닌 해당 주소에 저장되어있는 값을 확인
p *ptr@4 ptr이 가리키는 것이 4개 크기의 배열일 경우 4개의 값을 모두 출력
p $eax 특정 레지스터의 값을 확인
p 'file.c'::myVariable 특정 파일의 전역변수(myVariable)을 확인
p myFunc::myVariable 특정 함수의 static 변수의 값을 확인
p (char *)ptr 특정 형식으로 변환해서 값을 출력
void *ptr = "abc"; 와 같은 경우  
p myVariable = 10 myVariable의 값을 10으로 설정
p ptr + 4 특정 byte-offset 위치를 지정하여 출력
p/{출력 옵션} myVariable 여기서 출력 옵션에 들어갈 수 있는 값을 다음과 같다.
- t: 2진수로 출력  
- o: 8진수로 출력  
-d: 부호가 있는 10진수로 출력  
-u: 부호가 없는 10진수로 출력  
-x: 16진수로 출력  
-c: 최소 1바이트 값을 문자형으로 출력  
-f: 부동 소수점 형식으로 출력  
display myVariable 변수의 값을 매번 디스플레이한다.
n, s 명령어를 통해 라인 단위로 실행하며 값을 확인하고자 할 때 유용  
display/{출력 옵션} myVariable 출력 옵션에 맞추어 값을 출력
p 명령어의 출력옵션과 동일  
undisplay {display_ID} 디스플레이 설정을 없앤다
disable display {display_ID} 디스플레이를 비활성화 한다
enable display {display_ID} 디스플레이를 활성화 한다

스택 프레임의 정보를 이용해 디버깅

보통 프로그램을 실행하여 특정 부분에서 오류가 발생해 종료된 경우 스택이 깨지지 않은 이상 스택 프레임은 종료될 당시의 상태를 유지한다. 이 때, bt(backtrace)명령어를 통해 전체 스택프레임 정보를 확인할 수 있다. 이와 관련하여 스택프레임에 대한 자세한 명령어들은 다음과 같다.

명령어 설명
info frame 스택 프레임의 정보를 출력
frame {frame_number} 특정 스택 프레임으로 변경
up 상위 스택 프레임으로 이동
up N N번 상위 스택 프레임으로 이동
down 하위 스택 프레임으로 이동
info args 함수가 호출될 때 인자를 출력
info locals 함수의 지역 변수를 출력
info catch 함수의 예외 핸들러를 출력

 

기타 유용한 명령어

어셈블리 코드 보기

  • disas myFunc: func 함수의 어셈블리 코드를 보여준다.

call 명령을 통한 함수별 모듈 테스트

  • call myFunc(123): 현재 상태에서 임의로 함수를 출력한다. 이는 리턴값을 추가로 출력해준다.

jump 명령을 통한 임의의 행, 주소, 함수로 분기

  • jump *0x0804000: 해당 주소로 무조건 분기
  • jump 10: 10번 라인으로 분기
  • jump myFunc: myFunc 함수로 분기

프로세스에 시그널 보내기

  • signal SIGKILL: 현재 수행중인 프로그램에 signal을 보낸다.

메모리 특정 영역에 값을 설정

  • set {{type}}{address} = {value}:
    이는 set {int}0x804000 = 100와 같이 사용되며 특정 메모리 영역에 값을 설정하게 해 준다.
    또한 이는 p *0x804000=100와 같은 결과를 가져다 준다.

gdb 환경 설정 관련 명령

명령 설명
info set set 명령으로 설정가능한 명령어들의 리스트를 보여준다.
set print array on array를 출력할 때 여러 줄로 출력해 준다.

$2 = {0x804000 "one",
0x804004 "two",
0x804008 "three",
0x0}
info functions 함수들의 리스트를 출력
info types 선언된 타입들을 출력

링킹이란?

  • 링킹은 컴파일과 링킹에서 마지막 과정으로 조각난 오브젝트 파일들을 하나의 바이너리 이미지로 합치는 과정이다.
  • ELF 포맷으로 되어있는 각 오브젝트를 섹션 종류별로 하나의 오브젝트로 합치는 과정이다.
  • $ ld -o main main.o sub.o
  • ld는 링킹 과정에서 각 오브젝트를 같은 종류의 섹션(text, data, bss 등)별로 묶어 하나의 ELF 바이너리로 만든다 : 결합
  • 결합과정 후에 합쳐진 각 섹션을 실제 코드에 맞게 조정한다. 이 때, 바이너리에 각 심볼이 가질 실제 주소를 구하고 이를 참조하는 부분에 주소를 설정한다. : 재배치

ELF 바이너리 포맷

NOTE

링킹과정에서 주요하게 알아두어야 할 부분은 ELF 바이너리 포맷과 링커 스크립트이다. 따라서 링킹 과정에 대해 자세히 알아보기 전 간략하게 ELF 바이너리 포맷에 대해 알아두고 넘어가자.

ELF 바이너리 포맷의 구조

: ELF 바이너리 포맷의 구조는 크게 ELF header, Program header table, Section & Segment area, Section header table 로 구성되어 있다. 여기서 Program header table에는 재배치 불가능한 ELF 오브젝트(실행 바이너리), Section header table에는 재배치 가능한 ELF 오브젝트(*.o 바이너리)가 온다.

  • readelf -h {executable binary}: ELF header를 조회한다.

  • readelf -l {executable binary}: 실행 바이너리 파일의 프로그램 헤더 테이블, Segment Section을 조회한다.

    • 프로그램 헤더의 내용에는 다음을 포함한다.

      LOAD 0x000000 0x... 0x... 0x... 0x... RWE 0x...

      여기서 맨 첫번째 값(타입)에 올 수 있는 값들의 종류는 다음과 같다.

      타입명 의미
      LOAD 메모리에 로드해야 되는 세그먼트
      NULL 사용되지 않는 프로그램 헤더
      DYNAMIC 동적 링크를 해야 되는 자료를 포함하는 세그먼트
      INTERP 인터프리터 이름을 포함하는 세그먼트
      PHDR 프로그램 헤더를 포함하는 세그먼트
      NOTE 노트 정보를 포함하는 세그먼트
      SHLIB 예약된 프로그램 헤더
    • Segment Section에는 위의 프로그램 헤더의 각 세그먼트가 어떤 섹션들로 이루어져 있는지를 나타낸다.

ld를 이용한 링킹

  1. Application Linking

    • 재배치 가능한 오브젝트( file.o )의 결합과정

    • 재배치 과정을 통해 재배치 불가능한 오브젝트( 실행 바이너리 main )를 만들어 내는 과정

      /usr/libexec/gcc/i386-redhat-linux/4.1.2/collect2
      -o test  
      /usr/lib/gcc/crt1.o  
      ...  
      main.o func.o
      -lgcc —as-needed -lgcc\_s —no-as-needed -lc -lgcc —as-needed -lgcc\_s —no-as-needed  
      /usr/lib/gcc/crtend.o

      위와 같은 명령을 위해선 main.o , func.o 가 존재해야하는데, 이는 $ gcc -c *.c 명령을 통해 얻을 수 있다. 그리고 위 명령어는 아래와 동일한 값을 가지기도 한다.

      ld -dynamic-linker /lib/ld-linux.so.2 
      -o main /usr/lib/crt1.o /usr/lib/crtn.o main.o func.o -lc

      -dynamic-linker lib/ld-linux.so.2 : 동적 링커로 ld-linux.so.2 를 사용한다. 이는 .interp 섹션에 해당 동적 링커의 경로가 들어가 main 프로그램 실행 시 해당 링커를 메모리 주소 공간이 같이 로드해 먼저 수행한다. 이후, 동적 링커는 프로그램 실행에 필요한 공유 라이브러리들을 확인하여 메모리에 로드 및 주소공간을 맵핑한다.

      NOTE
      -dynamic-linker lib/ld-linux.so.2 옵션을 주지 않으면 링커는 -lc 옵션을 동적링커로 인식하여 오류를 발생시킴에 주의하자.

      아키텍처에서 지원하지 않는 연산이 사용되어 gcc의 내부 함수로 바뀐 경우, 해당 연산에 대한 라이브러리(libgcc.a, libgcc_s.so)가 필요한 경우가 있다. 이 때, 이러한 라이브러리가 추가되어야 한다.

      ld -L/usr/lib/gcc/i386-redhat-linux/4.1.2
      -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o  
      main.o func.o -lc -lgcc -lgcc_s
  2. -l 옵션으로 라이브러리 지정

    : 링킹시 특정 라이브러리를 포함하려면 -l{lib_name} 을 사용한다. 만약 포함할 라이브러리가 libJay.so 라면 -lJay 만을 기입하면 충분하다.

    : -l 옵션은 반드시 소스보다 뒤에 와야한다. 이는, ld가 C 소스 파일에서 호출한 함수들을 라이브러리에서 찾을 때 명령라인 입력을 기준으로 한 번만 스캔해서 찾아 링킹하기 때문이다. 만약 이를 위반할 경우 ld가 해당 라이브러리의 심볼들을 resolve 하지 못하는 문제가 발생한다.

  3. -L 옵션으로 라이브러리 검색 디렉토리 지정

    : ld는 기본적으로 /usr/i386-redhat-linux/lib, /usr/local/lib, /lib, /usr/lib 디렉토리 순서로 라이브러리를 찾는다. 이 경로에 없는 라이브러리의 경우 위 옵션을 이용해 경로를 지정해 주어야 한다.

  4. -static 옵션으로 정적 링킹

    : 공유 라이브러리와 동적 라이브러리가 동시에 있는 경우 위 옵션을 사용하면 정적 라이브러리와 링킹한다. 정적 링킹 시 포함되는 모든 라이브러리의 정적 라이브러리가 있어야 한다.

     ld -static -L/usr/lib/gcc/i386-redhat-linux/4.1.2 -o test main.o func.o 
     -\( -lgcc -lgcc_eh -lc -\)

    : -\( -lgcc -lgcc_eh -lc -\) 옵션은 앞의 라이브러리에 대해 참조하는 모든 심볼을 완전히 찾을 때까지 라이브러리들을 계속 스캔하면서 찾는 옵션이다. 이는 ld가 기본적으로 정적 라이브러리에 있는 오브젝트를 링킹하고자 할 때 명령 라인에서 주어지는 순서대로 한 번만 스캔하면서 심볼의 참조를 찾고, 해당 심볼을 정의하는 정적 라이브러리 내의 해당 오브젝트만 링킹하기 때문이다.

  5. -shared 옵션으로 공유 라이브러리와 링킹

    : 정적 라이브러리와 공유 라이브러리가 동시에 존재할 경우 공유 라이브러리와 링킹한다. 이 옵션은 default 값으로 -static 을 지정하지 않으면 자동으로 지정되는 옵션이라 보면 된다.

  6. -r 옵션으로 부분 링킹

    : 재배치 가능한 오브젝트들을 링킹해 하나의 큰 재배치 가능한 오브젝트로 만드는 옵션이다. 이는 나중에 다시 링킹될 수 있다.

     ld -r -o Operator.o plus.o minus.o mult.o div.o
    
     Prev           |  Next
     plus.o minus.o |  Operator.o
     mult.o div.o   |

gcc를 이용한 컴파일 옵션

다음의 gcc 컴파일 명령어를 통해 사용되는 옵션에 대해 간략히 알아보자.

gcc -v -I/usr/local/include -DDEBUG -Wall -W -O2 -L/usr/local/lib -o like like.c -lm
  • v: gcc C컴파일러의 옵션으로 컴파일 과정을 출력한다. (verbose)
  • I/usr/local/include -DDEBUG: 전처리기(cc1 -E)의 옵션
  • Wall -W -O2: cc1의 옵션
  • L/usr/local/lib -o like like.c -lm: collect2의 옵션. 이는 ld를 호출 시 전달한다.

man gcc 명령을 통해 기능별 옵션 리스트를 볼 수 있다.

gcc 옵션 규칙

  1. 각각의 옵션에는 그에 상응하는 gcc 내부 변수가 존재한다.

    • gcc/options.c, gcc/options.h에는 gcc 옵션에 상응하는 변수들을 볼 수 있다.

      extern int warn_unused_variable;
      extern int flag_branch_target_load_optimize;
      ...
      
    • 예를 들어, int warn_unused_variable 변수는 Wunused_variable 옵션을 주면 값이 설정되는 변수이다. 아무런 옵션도 주지 않으면 기본 값이 0으로 설정된다.

  2. [f|W|m][옵션]은 설정이고, -[f|W|m]no-[옵션]은 해제이다.

    • Wno-unused_function을 이용해 0으로 해제한다.
  3. 동일 종류의 옵션은 최종 옵션만 옵션 변수에 설정된다.

    • gcc ... -W-unused_function -Wno-unused_function 은 마지막 설정인 -Wno-unused_function이 적용된다.
  4. 그룹 옵션이 존재한다.

    • fdump-tree-all 은 -fdump-tree-original, -fdump-tree-optimize ... 의 옵션을 동시에 준 것과 동일하다.
    • O1, -Wall이 그러하다.
  5. f는 플래그, -W는 경고, -m은 아키텍처 종속, 기타 나머지들이다.

gcc 필수 옵션 및 중요 옵션

  1. gcc C 컴파일러 드라이버 옵션
    • -E: 전처리 과정의 결과를 화면에 보인다.
    • -S: 어셈블리 파일(*.s)만 생성하고 컴파일을 멈춘다.
    • -c: 오브젝트 파일(*.o)만 생성하고 컴파일을 멈춘다.
    • -v: 컴파일 과정을 화면에 자세히 출력한다.
    • -save-temps: 컴파일 과정의 중간파일인 전처리 파일(*.i), 어셈블리 파일(*.s), 오브젝트 파일(*.o)를 지우지 않고 디렉토리에 저장한다.
  2. cc1 -E 또는 cpp0 전처리기 옵션
    • -I: 전처리 과정 중 헤더파일을 탐색하는 기본 디렉토리를 추가한다. 기본적으로 #include <file> 형식은 시스템 헤더 디렉토리로 설정된 /usr/local/include, /usr/lib/gcc/i386-redhat-linux/4.1.2/inclue, /usr/include 순으로 찾고, #include "file"형식은 현재 디렉토리에서 먼저 찾고 위 순서로 찾는다. 따라서 -I옵션으로 디렉토리를 지정하면 시스템 헤더 디렉토리보다 우선해서 찾는다.
    • -include {file}: 헤더파일을 소스 내에 추가할 때 사용한다. 이는 #include "file"한 것과 동일한 결과를 나타낸다.
    • -D{macro_name}: 매크로를 외부에서 define할 때 사용한다. 이는 디버깅에서 많이 쓰인다.
    • -D{macro_name}={value}: 이는 #define macro value한 것과 동일한 결과를 나타낸다.
    • -U{macro_name}: 이는 -D와 반대의 결과를 나타내고, #undef macro와 동일한 결과를 나타낸다.
    • -M, -MM: (Makefile작성 시 유용한 )종속 항목(컴파일되기 위해 필요한 항목) 리스트를 출력한다. -M은 make를 위한 소스 파일의 모든 종속 항목을 출력하고, -MM은 기본 include 디렉토리에 있는 헤더 파일은 빼고 종속 항목을 출력한다.
    • -nostdinc: 디폴트 include 디렉토리(/usr/include 등)에서 헤더 파일을 탐색하지 않고, -I 옵션으로 추가한 디렉토리에서만 헤더 파일을 탐색한다.
    • -C: 전처리 과정에서 주석을 제거하지 않는다.
    • -Wp,{options}: 전처리기의 옵션으로 전달하고자 할 때 사용한다. 이는 전처리기(cc1 -E)와 C 컴파일러 드라이버(gcc)의 옵션이 서로 동일하다면 해당 옵션은 C 컴파일러 드라이버의 옵션으로 처리되므로 gcc의 해석을 거치지 않고 전처리기로 옵션을 전달하고자 할 때 사용된다.
  3. cc1 C 컴파일러 옵션
    • -ansi: ANSI C 표준에 부합하는 소스를 작성하려 할 때 사용된다.
    • std={C std}: C 표준 버전을 지정하고자 할 때 사용된다. 여기에는 c89, c99 등이 있다.
    • -traditional: Traditional C 문법으로 문법을 검사한다.
    • -W, -Wall: -Wall은 모든 모호한 문법에 대한 경고 메시지를 출력한다. -W은 합법적이지만 모호한 코딩에 대해 부가적인 정보를 제공한다. 따라서 두 옵션을 모두 사용하는 것이 좋다. gcc -W -Wall -o main main.c
    • -w: 모든 경고 메시지를 제거한다. 이는 -Wall, -W{options}보다 우선시된다.
    • -O0: 어떤 최적화도 수행하지 않는다. 아무런 최적화 옵션을 주지 않으면 -O0를 준 것과 동일하다.
    • -O1, -O2, -O3: 해당 옵션 적용시 세부적인 사항은 다음의 링크를 통해 확인할 수 있다.
    • -Os: 사이즈 최적화를 수행한다. 이는 임베디드 시스템 같이 메모리 공간이 협소한 곳에서 자주 사용된다.
    • -g: 디버거에 제공하는 디버깅 정보(변수 타입, 전역 심볼명, 주소, 소스 라인정보 등)를 바이너리에 삽입한다. 이는 readelf -a file 명령으로 읽을 수 있으며, .debug_*로 시작되는 섹션들이 이에 해당한다. 이 또한 -O옵션처럼 레벨이 있는데 -g0는 아무런 디버깅 정보를 삽입하지 않고, -g-g2와 동일하다.
  4. as의 옵션
    • -Wa,{as options}: 여기서 {as options}에 들어가는 옵션들은 다음과 같다.
      -al: 어셈블 된 인스트럭션을 보여준다.
      -as: 정의된 심볼을 보여준다.
      -I{path}: include 디렉토리를 지정한다.
      -W, --no-warn: 경고 메시지를 출력하지 않는다.
      -march={architectur}: 해당 어셈블리로 번역한다.
  5. collect2, ld의 옵션
    • -L{library_path}: 라이브러리를 찾을 디렉토리를 지정한다. 기본적으로는 /usr/i386-redhat-linux/lib, /usr/local/lib, /lib, /usr/lib 순으로 찾는다.
    • -l{name_library}: 같이 링크할 라이브러리를 지정한다. 이 옵션은 main()함수가 있는 소스 파일의 뒤에 와야한다. 만약 그렇지 않다면 undefined 참조 오류가 발생할 수 있다. 이는 링커가 아카이브를 찾는 데 있어 항상 main()함수 뒤에서 탐색하기 때문이다.
    • -shared: 공유 라이브러리와 정적 라이브러리가 같이 있을 경우 공유 라이브러리를 우선해서 링크한다. 이는 디폴트 옵션이기도 하다.
    • -static: 정적 라이브러리를 우선하여 링크한다. 이 경우, 파일의 사이즈가 커짐에 유의하자.
    • -nostdlib: 링크 시 표준 C 라이브러리를 사용하지 않는다.
    • nostartfiles: crt1.o, crti.o, ctrbegin.o, crtend.o, crtn.o와 같은 crt 오브젝트 파일들을 링크하지 않는다. 이는 OS, bootloader 같은 프로그램을 컴파일 시 사용된다.
    • -Wl{liker_options}, -Xliner f{liker_option}:
      gcc -o main main.c -Wl,-M,-s: collect2를 거쳐 ld에 -M -s 옵션이 전달된다.
      gcc -o main main.c -Xlinker -M -Xlinker -s: 이 또한 위와 같다.
    • 다음은 사용 가능한 유용한 링커 옵션들이다.
      -s: 실행 파일에서 심볼 테이블 제거
      -x: 출력 파일에서 로컬 심볼 제거
      -e{name}: 시작 심볼을 name 심볼로 사용한다. 기본적으로 시작 심볼은 _start 심볼이다.
      -M: 링커 스크립트와 메모리 맵을 자세히 출력한다.

gcc를 이용한 최적화 컴파일

  1. machine dependent 옵션을 이용한 최적화
    : -march={cpu_type} -mutne={cpu-type} -mcpu={cpu-type}:
    • -march 옵션은 cpu-type으로 지정하는 프로세서의 instructio set으로 코드를 생성한다. 이는 아래에서 설명할 -mtune을 내부적으로 포함하므로 -mtune을 굳이 주지 않아도 무방하다.
    • -mtune 옵션은 인스트럭션을 스케쥴링하거나 정렬할 때 해당 CPU에 최적화되게 스케쥴링 및 정렬한다. 이 때에는 CPU 파이프라인 단계, 각 인스트럭션을 수행하는 데 걸리는 사이클, 버스, 캐시 크기 등을 고려한다.
    • -mcpu 옵션은 -mtune옵션과 거의 동일하다.
    • cpu-type으로 generic을 사용할 수 있는데, 이때 -march에서는 사용할 수 없다. 이는 CPU 종류마다 인스트럭션 셋이 다르기 때문이다.
  2. 프로파일 결과를 이용한 최적화
    : 컴파일한 프로그램을 한 번 더 수행해 프로그램의 동작 특성을 파악하여, 이 정보를 바탕으로 다시 컴파일하며 최적화하는 방법이다.
    1. gcc -O2 -o main main.c -fprofile-generate를 통해 main 바이너리에 프로파일 정보를 출력하는 코드가 추가하여 컴파일한다.
    2. $ ./main을 통해 프로그램을 수행한다.
    3. gcc -O2 -o main main.c -fprofile-use를 통해 프로그램을 수행하며 수집한 정보를 바탕으로 좀 더 최적화하여 바이너리를 생성한다.
  3. 레지스터 활용을 통한 최적화
    • -fforce-mem: 메모리에 있는 값을 레지스터에 로드해 연산을 수행한다. (GCC 4.2 below)
    • -fforce-addr: 메모리 주소 값을 레지스터에 로드 후 연산을 수행한다.
  4. gcc 컴파일 속도 최적화
    • -pipe: 컴파일 과정에서 임시파일을 생성해 전달하지 않고 파이프로 전달해 컴파일 속도를 더 빠르게 한다.

기타

  1. 환경변수를 사용한 gcc 설정
    • COMPILER_PATH: gcc는 COMPILER_PATH에 등록된 전처리기(cc1 -E), 컴파일러(cc1), 어셈블러(as), 링커(collect2, ld)를 찾는다. 따라서 export COMPILER_PATH={directory}를 통해 원하는 전처리기, 컴파일러, 어셈블러, 링커를 선택케 할 수 있다.
    • C_INCLUDE_PATH, CPLUS_INCLUDE_PATH, OBJC_INCLUDE_PATH: 소스 파일을 전처리 할 때 헤더파일을 찾을 디렉토리를 지정하는 환경변수이다.
    • LIBRARY_PATH: 라이브러리를 찾을 디렉토리를 지정하는 환경 변수이다.
  2. specs 파일을 이용한 gcc 설정

전처리 과정 with cc1 -E

전처리과정은 cc1 -E에 의해 수행되는데, 이는 크게 두 부분으로 나눌 수 있다.

  1. 헤더 파일의 삽입
    • 시스템 헤더파일 디렉토리로 등록된,
      /usr/local/include
      /usr/lib/gcc-lib/i386-redhatlinux/3.2.2/include
      /usr/include
      순으로 헤더파일을 찾는다.
    • 기본 탐색 include 디렉토리를 추가하기 위해서는 gcc 옵션으로 I{include_directory}를 붙이면 해당 디렉토리를 먼저 탐색할 수 있게 한다.
  2. 매크로 치환 및 적용
    • #define된 부분은 심볼테이블에 저장되고, 심볼 테이블에 들어있는 문자열과 같은 문자열을 만나면 해당 내용으로 치환된다.
    • #ifedf와 같은 매크로를 처리하기 위해서는 cpp0 -D{define_option}을 지정해 주면 된다. 이는 #define {define_option}을 해 준것과 같다.
    • 시스템 관련된 predefined macro(__linux__ 등)는 이전 전처리기(cpp0)에서는 사용자가 명시적으로 predefine 해야했으나 cc1 -E에서는 내부적으로 알아서 predefine 된다.

어셈블 과정 with cc1

어셈블 과정은 cc1에 의해 수행되는데, 이는 크게 세 부분으로 나뉠 수 있다. front-end, middle-end, back-end 가 그것이다.

  1. front-end
    • 언어 종속적인 부분을 처리한다. 소스코드가 올바르게 작성되었는지 분석하고 GIMPLE 트리를 생성한다.
    • 어휘 분석, 구문 분석, 의미 분석, 중간 표현 생성을 수행한다.
  2. middle-end
    • GIMPLE 트리를 이용해 아키텍처 비종속적인 최적화를 수행후 RTL(Register Transfer Language: 고급언어와 어셈블리 언어의 중간 형태)을 생성한다.
    • GIMPLE 트리를 받아 SSA(Static Single Assignment) 형태로 변환한 후 아키텍쳐 비종속적인 최적화를 수행한다. 그 후 back-end에서 사용할 RTL을 생성한다.
      • 아키텍처 비종속적인 최적화의 리스트
        • return value optimizations
        • full redundancy elimination
        • conditional constant propagation
        • dead code elimination
        • copy propagation
        • value range propagation
        • branch prediction and profiling
        • dead store elimination
        • etc.
      • RTL 표현을 보기 위해서는 gcc -o main main.c -da와 같이 da 옵션을 주면 된다.
  3. back-end
    • 아키텍처 비종속적인 최적화와 아키텍처 종속적인 최적화가 함께 수행된다. 그 후, 목적 코드(어셈블리 코드)를 생성한다.
      • 아키텍처 종속적인 최적화의 리스트
        • 각종 고전 최적화
        • common sub-expression elimination
        • GCSE/PRE
        • constant propagation
        • copy propagation
        • cross-jumping, block merging
        • basic block re-ordering
        • IF-conversion
        • 각종 loop 최적화
        • loop un-rollling
        • 연산 강도 경감
        • loop 내의 불변 코드 옮김(Invariant code motion)
        • Instruction scheduling
        • register allocation
        • satifying operand constraints
    • 여기서는 Instruction scheduling, Register allocation이 일어난다.

기계어 코드 생성 with as

NULL

링킹 with collect2

정적 라이브러리와 동적 라이브러리란?

정적 라이브러리

파일 이름이 lib{name_of_library}.a의 형식을 가진다.
/usr/lib/ 디렉토리에 위치한다.
ar -x /usr/lib/libc.a를 수행하면 해당 라이브러리를 풀어 어떤 파일들로 구성되어 있는지 확인할 수 있다.
추가로, libc.a는 ioputs.o 등과같은 목적파일들로 구성되어 있다고 할 때, 이러한 목적 파일에 어떠한 함수가 들어있는지 확인하려면 nm ioputs.o 명령을 수행하면 된다.
이러한 오브젝트들을 gcc 옵션 중 -static 옵션을 주면 main.o와 링크되어 삽입된다.
collect2는 내부적으로 링커인 ld를 호출해 링킹과정을 수행하므로 실제로는 ld -o main main.o scanf.o printf.o가 수행된다.
정적 라이브러리를 이용한 컴파일은 실행 파일에 같은 오브젝트가 중복되어 링크되므로 메모리 공간을 낭비한다는 것이다.

공유 라이브러리

공유 라이브러리를 이용해 오브젝트와 링킹할 때에는 해당 함수를 직접 호출하지 않는다. 대신, PLT(Procedure Linkage Table) 섹션의 특정 영역을 호출하게 링크한다. 여기서, PLT 섹션의 코드에는 GOT(Global Offset Table)(정확히는 GOT.PLT)섹션의 해당 함수 엔트리에 있는 값을 in-direct로 점프하게 되어있다.
이는 프로그램 실행 중, system call을 발생시키고 이는 ELF 헤더를 읽어 동적 링커를 사용한다는 정보를 얻어 수행에 필요한 섹션들과 동적 링커(/lib/ld-linux.so.2)를 프로세스 주소 공간에 로드한다. 그 후, 제어권을 동적 링커에게 넘긴다.
공유 라이브러리는 lib{name_of_library}.so.{major_version}.{minor_version}.{patch_version}으로 구성되어 있다.
표준 C 공유 라이브러리와 동적 링커는 ldd {name_of_library}로 확인할 수 있다.

다음은 collect2의 수행 모습에 대한 간략한 코드와 이에 대한 설명이다.

.../collect2 --eh-frame-hdr -m elf_i386 
--hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 -o like
/usr/lib/gcc/crt1.o
/usr/lib/gcc/crt2.o
-L/usr/lib/gcc
like.o
-lgcc --ass-needed -lgcc_s --no-as-needed -lc
/usr/lib/gcc/crtend.o
/usr/lib/gcc/crtn.o
  • m elf_i386: 바이너리 포맷과 CPU 타입 지정
  • dynamic-linker /lib/ld-linux.so.2: 동적 링커로 ld-linux.so.2를 지정
  • -o like: 링크 과정이 끝나고 생성되는 실행 파일명은 like
  • L/usr/lib/gcc: 라이브러리 파일을 찾을 디렉토리를 지정
  • lgcc: 링크할 라이브러리를 지정

-l 옵션 뒤에오는 것이 라이브러리 명이다. libc.so(libc.a)의 경우 -lc가 된다.
만약 정적 라이브러리와 동적 라이브러리가 동시에 존재한다면 동적 라이브러리 가 우선시 된다.

crt1.o, crtend.o 등의 파일은 초기화 루틴 & 종료 루틴을 위해 링크된다.

들어가기 전

GCC에 대해 알아보기 전, C 소스의 컴파일 과정에 대해 간략히 짚어보고 들어가자.

C의 컴파일 과정

NOTE

gcc와 GCC는 전혀 다른 의미이다.
gcc는 GNU C Compiler의 약어이고,
GCC는 GNU Compiler Collection의 약어이다.

/usr/bin/gcc는 엄밀한 의미에서는 C 컴파일러가 아니다.
/usr/bin/gcc는 내부적으로 전처리기인 cpp0 또는 cc1 -E명령을 통해 전처리 과정을 수행하고
실제적인 C 컴파일러인 cc1을 통해 컴파일한 후
어셈블러인 as를 통해 목적 코드를 만들고,
링커를 호출하는 collect2를 호출해 collect2가 실제적인 링커 ld를 호출함으로써 라이브러리와 링크해 실행 파일로 만들어낸다.

따라서 진짜 C 컴파일러는 /usr/libexec/gcc/i386-redhatlinux/4.1.2/cc1이라 할 수 있다.

다음은 /usr/libexec/gcc/i386-redhatlinux/4.1.2/ 내부에 있는 실행파일의 역할에 대한 설명이다.

  • cc1: C 컴파일러, -E 옵션을 붙이면 전처리기로 동작
  • cc1plus: C++컴파일러, -E 옵션을 붙이면C++ 전처리기로 동작
  • cpp0: 전처리기(old ver, 현재는 컴파일러에 전처리 과정이 통합되어 있다)
  • cc1obj: Objective-C 컴파일러, -E 옵션을 붙이면 전처리기로 동작
  • collect2: 링커(내부적으로 링커인 ld를 호출해 링크)
  • ld: 링커(실제로는 존재하지 않지만 설명을 위해 써 놓음)

다음은 /usr/bin/ 디렉토리에 존재하는 컴파일러 드라이버들이다.

  • cc: C 컴파일러 드라이버, gcc와 동일
  • gcc: C 컴파일러 드라이버
  • c++: C++ 컴파일러 드라이버, g++과 동일
  • g++: C++ 컴파일러 드라이버

위 컴파일러 드라이버는 전처리기, 컴파일러, 어셈블러, 링커를 각각 호출해 주는 역할만을 담당한다. 또한 자신이 지원하는 언어의 소스가 아닐지라도 해당 소스에 맞는 전처리기와 컴파일러를 호출해 주기도 한다. 따라서 gcc가 C++ 소스를 컴파일 할 수도 있는 것이다. 하지만 C++ 소스를 컴파일하기 위해 기본적으로 요구되는 라이브러리를 알지 못하므로 lstdc++ 옵션을 컴파일 명령시 추가해 주지 않으면 오류가 발생할 것이다.

다음은 간략한 컴파일 과정 전개도이다.

main.c --'cc1 -E'--> main.c 전처리 결과 --'cc1'--> main.s 어셈블리 파일 --'as'--> like.o 오브젝트 파일 --'ld|collect2'--> main.exe

이러한 과정들을 직접 눈으로 확인하고 싶다면 다음의 명령어를 사용하라.

gcc -v -save-temps -o main main.c

여기서 -v 옵션은 verbose의 약어로 컴파일 과정을 화면으로 출력하는 옵션이고,
-save-temps 옵션은 컴파일 과정 중 발생되는 중간 파일을 지우지 않고 저장하는 옵션이다.

+ Recent posts