2011. 7. 5. 17:57

[ 빌트인 함수 ]

♧ gcc에는 표준 라이브러리에 있을 듯한 몇몇 함수가 빌트인 함수로 마련되어 있음.

최적화 방법에 따라 소스에 쓰인 것과 다른 코드를 생성할 경우 있다.

   

▷ 예를 들면 printf(3) 함수의 경우 다음과 같이 문자열을 출력하는 코드에서는 실행할 때

pirntf(3)의 형식 문자열을 해석할 수 없으므로 다음과 같이 puts(3)를 호출하는 코드를 생성.

▷ 기본적으로는 같은 동작을 하고 더 빠르게 실행될 듯한 코드가 생성되어 문제는 없으나,

LD_PRELOAD 등으로 오버라이드(override)해서 동작을 변경하고자 할 경우에는 주의 할

필요가 있다. 최적화를 통해 어떤 코드가 생성되는지 gcc/builtins.c에 프로그램 되어 있다.

   

void *__builtin_return_address (unsigned int LEVEL)

→ 함수의 리턴 주소, 즉 함수를 호출한 지점의 주소를 반환.

매개변수 LEVEL은 호출 지점을 몇 번 거슬러 올라갈지를 정수로 지정하는데

통상 0(현재 함수)을 지정. 일반적으로 x86인 경우 %ebp + 4에 해당하는 포인터 값이 됨

   

void *__builtin_frame_address (unsigned int LEVEL)

→ 함수의 프레임 포인터(지역 변수나 레지스터 저장영역을 가리키는 포인터)를 반환.

매개변수 LEVEL은 호출 지점을 몇 번 거술러 올라갈지를 정수로 지정하는데

통상 0(현재 함수)을 지정. 일반적으로 X86인 경우 %ebp 가 된다.

   

int __builtin_types_compatible_p (TYPE1, TYPE2)

→ 매개변수 TYPE1과 TYPE2가 서로 호환성이 있는지를 검사.

일반적으로 매크로를 이용해 타입에 맞는 적당한 함수로 dispatch할 때 사용.

   

TYPE __builtin_choose_expr (CONST _EXP, EXP1, EXP2)

→ 이 함수는 CONST_EXT? EXT1 : EXT2와 동일한 기능을 하지만, 컴파일할 때 결정.

__builtin_types_compatible_p를 매개변수 CONST _EXP로 사용해서 적당한 함수호출을

선택하는 형태의 매크로를 작성할 때 사용.

   

int __builtin_constant_p (EXP)

→ 매개변수 EXP가 정수인지를 판정.

인수가 정수임을 알았을 때 최적화된 코드를 선택하는 편이 더 좋을 경우에 사용.

   

long __builtin_expect (long EXP, long C)

→ 매개변수 EXP의 값이 매개변수 C값이 될 경우가 많을 것이라는 판단의 근거로,

분기를 최적화하고자 할 경우에 사용.

   

long __builtin_prefetch (const void *ADDR, int RW, int LOCALITY)

→ 매개변수 ADDR에 있는 데이터를 캐시에 prefetch하고자 할 경우 사용한다.

매개변수 RW가 1일 경우는 가까운 곳에 기록할 내용이 있음을,

0일 경우는 가까운 곳에 읽어야 할 내용이 있음을 나타낸다.

매개변수 LOCALITY는 0~3사이 값으로 0은 사용하면 바로 불필요한 데이터,

3은 사용하기 시작하면 당분간 계속 사용할 데이터임을 나타냄.

  

   

[ 애트리뷰트 (__attribute__) ]

♧ 함수에 애트리뷰트를 덧붙여 선언하면

함수에 특별한 의미를 부여하거나 함수호출을 최적화 할 수 있다.

   

▶ 애트리뷰트 선언 2가지 방법

① int foo(int n) __attribute__((애트리뷰트));

int foo(int n) { … }

   

② __attribute__((애트리뷰트)) int foo(int n) { … }

   

▶ 함수에 관한 애트리뷰트에는 다음과 같은 것들이 있다.

attribute

설명

constructor

main 함수가 호출되기 전이나 공유 오브젝트가 로드 되었을 때

실행해야 하는 함수에 사용.

destructor

exit하기 전이나 공유 오브젝트가 언로드 되기 직전에

실행해야 하는 함수에 사용.

cleanup

auto 변수가 주어진 영역에서 벗어나 없어질 때 호출되는 함수를 지정.

section

특정 섹션에 코드를 배치.

used

어디에서도 호출하지 않는 경우에도 코드를 반드시 생성.

어셈블러 코드에서 호출되는 경우에 사용.

weak

weak 심볼인 코드를 생성

alias

다른 심볼의 별칭(앨리어스)을 지정. 통상 weak와 함께 사용

visibility

심볼의 가시성을 제어하기 위해 사용.

'default', 'hidden' (공유 객체 밖에서는 볼 수 없음),

'protected'(공유 객체 내에서 호출할 때는 LD_PRELOAD로도

오버라이드 되지 않음) , 'initernal' (함수 포인터 등을 포함한 객체

외부에서의 호출을 수행하지 않음) 등...

stdcall

x86 호출 규약 중 하나.

매개변수에 사용한 스택을 호출된 쪽에서 pop하는 경우 사용.

sdecl

x86 호출 규약 중 하나.

매개변수에 사용한 스택을 호출한 쪽에서 pop하는 경우 사용.

fastcall

x86 호출 규약 중 하나. 처음 두 매개변수를 %ecx, %edx를 이용해 호출

regparm

레지스터에 전달할 매개변수의 개수를 제어.

vector_size

변수의 벡터 크기를 지정.

dllimport

MS 윈도우의 dllimport.

dllexport

MS 윈도우의 dllexport

pure

전역 변수와 매개변수에 따라서만 반환 값이 결정되고 부작용이 없는 경우에

사용된다. 경우에 따라서는 불필요한 호출을 생략할 경우가 있다.

const

매개변수만으로 반환 값이 결정되고 부작용이 없는 경우에 사용.

malloc

NULL 외의 반환 값이 다른 포인터와 공유되지 않은 경우에 사용.

noreturn

exit(2)와 같이 반환되지 않는 함수에 사용.

noinline

인라인 전개되기를 원하지 않는 경우에 사용.

always_inline

최적화 레벨이 낮아도 인라인 전개한다.

nothrow

예외를 throw 하지 않는 경우 사용.

format

형식 문자열이 printf, scanf, strftime, strfmon 중

어떤 스타일과 같은지를 가리키는 것으로,

형식 문자열과 가변 매개변수 간 대응을 검사하게 된다.

format_arg

어떤 매개변수가 형식 문자열인지를 가리킨다.

nonnull

NULL이 될 수 없는 포인터 매개변수를 가리킨다.

unused

사용하지 않을 경우에도 경고를 출력하지 않는다.

deprecated

사용될 경우에 경고를 출력.

warn_unused_result

반환 값을 검사하지 않을 때 경고를 출력.

no_instrument_function

-finstrument-functions인 경우에도

프로파일 함수를 호출하지 않도록 한다.

데이터에 관한 애트리뷰트 

aligned

변수 영역의 정렬을 제어.

packed

구조체 내부에 정렬에 의한 채우기(padding)를 최소화 한다.

common

변수를 common 영역에 배치.

nocommon

변수를 common 영역에 배치하지 않는다.

shared

DLL을 사용한 프로세스 전체로 공유된 매개변수에 사용.

  

   

   

[ 레이블(label) 참조 ]

♧ C에서는 그다지 사용되지 않지만 goto로 점프할 위치를 지정할 때 레이블을 사용.

goto error

……

error:

/* 에러 처리 */

   

▷ GCC에서 레이블은 &&로 참조하고 void *형 함수에 대입할 수 있다.

void * label;

label = &&error;

……

goto *label;

……

error:

이와 같이 레이블을 변수에 대입할 수 있기 때문에 변수 값에 따라 점프할 위치를 변경하도록

하는 코드는 레이블로의 참조를 요소로 갖는 배열로도 구현할 수 있다.

레이블을 &&로 참조한 값은 void *에 대입할 수 있는 포인터형이므로 뺄셈으로 오프셋을

얻을 수 있다. 따라서 다음과 같은 코드를 작성할 수 있다.

static int labals[] = { &&labal0 - &&label0, &&label1 - &&label0, ...};

goto * (&&labal0 + labels[ i]);


label0:

label1:

  

   

[ 정리 ]

GCC는 C99표준에 준거하도록 노력하는 한편, 소스코드를 기술하기 쉽도록 하기 위한

확장 기능을 제공하고 있다. GCC의 확장기능임을 이해한 후 이 기능을 사용하면 편리한

경우가 있다. 예를 들면 리눅스 커널에는 GCC확장 기능을 이용한 코드를 볼 수 있다.

이러한 코드를 파악하려 할 경우에는 GCC 확장기능에 대한 이해가 필수적이다.

Posted by devanix
2011. 7. 5. 03:55

♧ 통상 GNU/리눅스의 공유 라이브러리를 만들 때는

각각의 .C 파일을 PIC(Position Independent Code)가 되도록 컴파일 한다.

그러나 실은 PIC로 컴파일 하지 안아도 공유 라이브러리는 만들 수 있다.

그러면 굳이 PIC로 컴파일 하는 이유가 있는 것일까?

   

▶ fpic.c 작성

#include <stdio.h>

void func() {

printf("");

printf("");

printf("");

}

   

PIC로 컴파일하기 위해 gcc -fpic 또는 fPIC 옵션을 지정한다.

-fpic :: 좀더 고속으로 코드를 생성할 가능성이 있지만,

CPU에 따라 -fpic로 생성할 수 있는 GOT(Glocal Offset Table)의 크기에 제한이 있다.

-fPIC :: CPU에 관계없이 사용할 수 있다. 여기서는 -fPIC를 사용한다.

(※ x86에서는 -fpic와 -fPIC가 동일)

   

% gcc -o fpic-no-pic.s -S fpic.c

% gcc -fPIC -o fpic-pic.s -S fpic.c

위와 같이 생성된 어셈블리어의 소스코드를 보면 PIC 버전은 printf를

PLT(Procedure Linkage Table)를 경유해서 호출하는 것을 알 수 있다.

(※ 우분투 7.04에서 테스트)

   

다음에는 공유 라이브러리를 만든다.

% gcc -shared -o fpic-no-pic.so fpic.c

% gcc -shared -fPIC -o fpic-pic.so fpic.c

   

▷ 위 공유 라이브러리의 동적 섹션(dynamic section)을 readelf 명령으로 보면,

비 PIC공유 라이브러리에서는 TEXTREL 이라는 엔트리가 있고 (텍스트 내의 재배치 필요),

RELCOUNT(재배치 수)가 5로, PIC 공유 라이브러리보다 3만큼 크다.

그 이유는 printf()3회 호출하기 때문이다.

   

PIC 공유 라이브러리에서의 RELCOUNT0이 아닌 이유는 gcc가 기본적으로 사용하는

시작 파일에 포함된 코드 때문이다. gcc에 -nostartfiles 옵션을 지정하면 이 값은 0이 된다.

   

[ PIC와 비 PIC 공유 라이브러리의 성능 비교 ]

♧ 위 예에서는 비 PIC 버전은 실행 시에(동적 링크 시에) 5개의 주소가

재배치 되어야 한다고 했다. 그러면 재배치 수가 매우 커지면 어떻게 될 것인가?

   

공유 라이브버리를 PIC버전과 비 PIC버전printf()를 천만 번 호출 ( 셸 스크립트로 실행 비교)

▶ 다음 셸 스크립트를 실행하면 printf()를 천만 번 호출하는 공유 라이브러리를

비 PIC버전과 PIC 버전으로 만들고, 각각을 링크한 실행 파일 fpic-no-pic와 fpic-pic를 생성.

#! /bin/sh

rm -f *.o *.so

num=1000

for i in `seq $num`; do

echo "void func$i() { " > fpic$i.c

ruby -e "10000.times { puts 'printf(\"\");' }" >> fpic$i.c

echo "} " >> fpic$i.c

gcc -o fpic-no-pic$i.o -c fpic$i.c

gcc -fPIC -o fpic-pic$i.0 -c fpic$i.c

done

gcc -o fpic-no-pic.so -shared fpic-no-pic*.o

gcc -o fpic-pic.so -shared fpic-pic*.o

echo "int main() { return 0; }" > fpic-main.c

gcc -o fpic-no-pic fpic-main.c ./fpic-no-pic.so

gcc -o fpic-pic fpic-main.c ./fpic-pic.so

   

비 PIC 버전이 첫 회 2.15초, 두 번째 이후에는 약 0.55초가 걸리고,

% repeat 3 time ./fpic-no-pic

2.15s total : 0.29s user 0.48s system 35% cpu

0.56s total : 0.25s user 0.31s system 99% cpu

0.55s total : 0.30s user 0.25s system 99% cpu

   

PIC 버전은 최초 0.02초, 두 번째 이후에는 0.00초가 되었다.

% repeat 3 time ./fpic-no-pic

2.15s total : 0.29s user 0.48s system 35% cpu

0.56s total : 0.25s user 0.31s system 99% cpu

0.55s total : 0.30s user 0.25s system 99% cpu

   

☞ main()의 내용은 없으므로 비 PIC 버전은 동적 링크할 때

재배치에 2.15 ~ 0.55초를 필요로 함을 알 수 있다.

실행 환경은 Xeon 2.8GHz + Debian GNU/리눅스 sarge + GCC 3.3.5이다.

   

▷ 비 PIC 버전의 단점은 실행할 때 재배치에 시간이 걸린다는 점만이 아니다.

재배치가 필요한 부분의 코드를 재작성 하기 위해

『텍스트 섹션 내의 재배치가 필요한 페이지를 로드 → 재작성

→ copy on write 발생 → 다른 프로세스와 텍스트를 공유할 수 없음』

이라는 사태가 발생. 즉, 여기서는 텍스트(프로그램 코드)를 다른 프로세스와

공유할 수 있는 『공유』라이브러리의 주요한 장점이 사라지고 만다.

   

한편, 비 PIC 버전의 fpic-no-pic.so 와 PIC 버전의 fpic-pic.so 파일 크기를 비교하면,

전자는 268MB, 후자는 134MB로 크게 차이가 난다. readelf -S로 섹션 데이터를 보면

다음과 같은 차이가 있다.

   

컴파일러

.rel.dyn

.text

비 PIC

152MB

114MB

PIC

0MB

133MB

비 PIC 버전은 코드(.text) 크기는 PIC 버전보다 작지만, 재배치에 필요한 정보(.rel.dyn)가

상당한 용량을 차지.

   

[ 정리 ]

여기서는 공유 라이브러리를 작성할 때 PIC로 컴파일 해야 하는 필요성에 대해 알아보았다.

비 PIC 공유 라이브러리를 작성할 수는 있지만 실행할 때 재배치에 시간이 소요되고 다른

프로세스와 코드(.text)를 공유할 수 없는 커다란 단점이 있다. 따라서 공유 라이브러리를

작성할 때는 .c 파일을 PIC로 컴파일 하도록 한다.

 

Posted by devanix
2011. 7. 4. 22:44

♧ 같은 이름을 갖는 심볼의 충돌

C 또는 C++프로그램에서 같은 이름을 갖는 전역 심볼이 둘 이상 존재하면 어떻게 될까?

   

[ .o 파일을 모아서 링크할 경우 ]

▷ 우선 a.c와 b.c가 동일한 이름의 func()함수 정의

/* a.c */

#include <stdio.h>

void func() {

printf ("func() in a.c\n");

}

/* b.c */

#include <stdio.h>

void func() {

printf ("func() in b.c\n");

}

▷ main.c에서 func() 호출.

void func();

int main() {

func();

return 0;

}

   

▶ 위 세 파일을 각각 컴파일해서 실행.

☞ 정적으로 링크하려 하면 func() 함수가 여러 개 존재하므로 링커에서 에러를 출력.

이 에러 덕분에 예상외의 func() 함수가 호출되는 문제를 사전에 피해갈 수 있다.

  

   

   

[ 라이브러리를 생성해서 링크할 경우 ]

♧ 앞에서와 동일한 소스코드 a.c와 b.c에서 ar을 사용해

정적 라이브러리를 생성하면 링크할 때 에러가 발생하지 않는다.

그 이유는 링커와 달리 오브젝트 파일로 아카이브를 생성하는

ar 명령은 심볼 충돌을 검사하지 않기 때문.

% gcc -c a.c b.c main.c

% ar cr libfoo.a a.o b.o

% gcc main.o libfoo.a

   

▶ 생성된 실행 파일 a.out을 실행

☞ 실행해 보면 a.c의 func()를 호출함을 알 수 있다.

이는 링크할 때 libfoo.a에 있는 두 func() 중에 a.c의 함수를 b.c의 함수보다 먼저 찾기 때문.

   

▶ 정적 라이브러리 libfoo.a를 ar 명령으로 생성할 때

인자의 순서를 a.o b.o에서 b.o a.o로 변경하면 b.o의 func()가 호출.

   

▶ 또한 a.c와 b.c를 이용해 각각 정적 라이브러리를 만든 경우에도 링크할 때는

에러가 발생하지 않는다. 이때 gcc에 넘긴 liba.a와 libb.a의 순서를 반대로 하면

b.c의 func()를 호출하게 된다.

   

▶ 마찬가지로 a.c와 b.c로 각각 동적 공유 오브젝트 생성해서 링크하면

에러는 발생하지 않는다. 아래 예에서는 a.so의 func()가 호출.

   

링크할 때 명령행 인자의 a.so와 b.so의 순서를 바꾸면 b.so의 func() 함수를 먼저 찾게 된다.

동적 링크할 때는 기본적으로 먼저 발견한 심볼 정의가 사용된다.

   

▷ GNU C Library의 주요 개발 구성원인 울리히 드레퍼가 작성한

How to write Shared Libraries 에는 다음과 같이 기술 되어 있다.

"scope 내에 동일한 심볼 정의가 둘 이상 포함되어 있어도 상관 없다.

심볼을 찾는 알고리즘은 단순히 최초에 발견한 심볼 정의를 사용할 뿐이다.

....이 개념은 상당히 강력해서 .... 그 일례는 LD_PRELOAD의 기능을 이용하는 것이다."

  

   

   

[ C++와 같은 이름의 클래스 ]

C++에서 동일 심볼 문제는 예상치 않은 멤버 함수를 호출하는 문제를 야기한다.

   

소비세를 계산하는 s.{h, cpp}라는 소스코드와 이를 이용한 main.cpp가 있다고 하자.

▷ a.h 작성

/* a.h */

class Tax {

public:

int tax (int price);

Tax();

private:

double consumption_tax_;

};

   

▷ a.cpp 작성

/* a.cpp */

#include "a.h"

Tax::Tax() : consumption_tax_(1.05) {}

   

int Tax::tax(int price) {

return price *consumption_tax_;

}

   

▷ main.cpp 작성

/* main.cpp */

#include <iostream>

#include "a.h"

int main() {

Tax tax;

int apple_price =100;

std::cout << "apple: " << tax.tax(apple_price) << std::endl;

return 0;

}

▶ 위 소스코드 컴파일, 링크해서 실행하면 apple: 105라는 메시지 출력.

   

여기서 별도로 개발된 b.cpp가 존재하고 같은 이름의 클래스 Tax가 구현되어 있다고 하자.

b.cpp내의 Tax 클래스의 용도는 a.cpp와는 전혀 다르다.

▷ b.cpp 작성

/* b.cpp */

class Tax {

public:

int deduct (int income);

Tax();

private:

double deduction_rate_;

};

   

Tax::Tax() : deduction_rate_(0.1) {}

   

int Tax::deduct (int income) {

return int(income * (1.0 - deduction_rate_));

}

▶ 그리고 b.cpp를 b.so로 컴파일하고 불행히도 b.so, a.so, main.so 순으로

링크된다면 성가신 일이 발생한다.

이번 실행 결과는 apple: 10이다.

이는 Tax 객체의 생성자로 b.so 내의 Tax::Tax()가 호출되고, Tax 객체의 멤버함수로서

a.so 내의 Tax::tax()가 호출되었기 때문이다.

즉, 예상외의 생성자가 호출되어 버그의 원인이 되었다.

   

☞ 프로그램이 거대해질수록 발생가능성이 커진다.

따라서 namespace를 사용해서 충돌을 피해갈 필요가 있다.

심볼을 하나의 파일 내에 모두 넣는다면 이름 없는 namespace를 사용해도 될 것이다.

예를 들면 b.cpp 전체를 snamespace { … }로 둘러싸면 위와 같은 문제는 발생하지 않는다.

  

   

[ weak 심볼 ]

♧ 첫머리에서 .o 파일을 모아서 링크할 때는 동일한 이름의 심볼 충돌이 확인 했다.

그러나 weak 심볼이 존재하면 애기는 달라진다.

   

다음과 같은 프로그램 main.cpp를 생각해보자.

/* main.cpp */

#include <iostream>

class Foo {

public:

Foo (int x) : x_(x) {}

void func() {

std::cout << x_ << std::endl;

}

private:

int x_;

};

   

int main() {

Foo foo(256);

foo.func();

return 0;

}

▶ 컴파일 실행하면 256 출력.

   

여기에 main.cpp와는 전혀 별개로 개발된 다음과 같은 a.cpp가 있다.

a.cpp 내에도 클래스 Foo가 구현되어 있다.

/* a.cpp */

#include <iostream>

class Foo {

public:

Foo(int x);

private:

int x_;

};

Foo::Foo(int x) : x_(x * x) {}

▶ 컴파일한 a.o가 불행히 main에 링크된다면 매우 성가신 일이 발생한다.

   

☞ 이번 실행 결과는 256이 아니라 제곱수인 65536이 출력 되었다.

이때는 링크 순서가 main.o, a.o 또는 a.o, main.o 둘 다 상관 없이 결과는 변하지 않는다.

main.cpp 내에 정의된 생성자 Foo::Foo(int)가 아니라 a.cpp 내의 Foo::Foo(int)가

사용된 것은 분명하지만, 이런 문제가 발생하는 이유는 무엇일까?

   

▶ 〔weak 심볼〕이란 무엇인가?

원인은 main.o에 포함된 Foo::Foo(int)는 weak 심볼이고

a.o에 포함된 Foo::Foo(int)는 weak 심볼이 아니기 때문이다.

▷ 아래의 nm 출력 결과에서 W로 표시된 것이 weak 심볼을 나타내는 표시이다.

   

   

☞ weak 심볼에 관해서는 『Linkers & Loaders』(john R. Levine 저, Morgan Kaufmaun)

의 6장에 다음과 같이 설명되어 있다.

"ELF에서는 weak 참조 외에 weak 정의라고 하는 또 하나의 weak 심볼을

추가하고 있다. weak 정의는 일반적인 정의가 존재하지 않는 경우에 대역 심볼을

정의한다. 일번적인 정의가 존재하는 경우에는 weak 정의가 무시된다."

즉, a.o에 일반적인(비weak 인) 정의가 존재하므로 main.o에 있는

Foo::Foo(int)의 weak 정의는 무시되었다는 것이다.

일단 이 문제를 피해가려면 이름 없는 namespace를 사용해 링키지 링크를 막는 것이

효과적이다. 구체적으로 main.cppㅇ듸 클래스 Foo { … }주위에 namespace { … }로 둘러싼다.

   

▶ weak 정의와 중복 코드 제거

그런데 왜 main.o Foo::Foo(int) 및 Foo::Func()는 weak 정의가 되는 것일까?

그 이유는 g++는 인라인 함수를 weak 정의로서 컴파일 하기 때문이다.

(클래스 정의 내의 함수정의는 표준 C++ 규격에 따라 인라인 함수로 간주)

따라서 weak 정의는 .o파일 내의 .gnu.linkonce.t.*라는 이름의 섹션에 배치된다.

.gnu.linkonce.t.*는 링크할 때 중복 코드 제거에 사용되는 섹션이다.

클래스 정의는 대개의 경우 .h 파일 내에 기술되고,

.h 파일은 여러 개의 .cpp 파일에 #include 된다. 이 때문에 각각의 .o 파일에

포함된 weak 정의를 링크할 때 하나로 모을 필요가 있다.

인라인 함수가 weak 정의로 되는 것은 이 때문이다.

  

 

Posted by devanix
2011. 7. 4. 17:55

♧ C로 작성된 함수를 C++에서 호출하고자 할 경우,

혹은 반대로 C++로 작성된 함수를 C프로그램에서 호출하고자 할 경우.

   

[ C/C++와 심볼명 ]

▷ 아래의 dbg 함수를 C/C++컴파일러로 각각 컴파일해 보자.

/************

* dbg.c

************/

#include <stdio.h>

void dbg(const char *s) {

    printf("Log: %s\n", s);

}

# C 컴파일 :: gcc -W -Wall -c dbg.c

# C++ 컴파일 :: g++ -W -Wall -c dbg.c

   

▷ C컴파일 / 심볼명 확인

▷ C++컴파일 / 심볼명 확인

   

▷ 컴파일해서 생성된 오브젝트 파일에 포함된 심볼명.

컴파일러

심볼명

C

dbg

C++

_Z3dbgPKc

   

☞ C 컴파일러로 함수를 컴파일 하면 기본적으로는 함수명이 그대로 심볼명이 된다.

시스템 환경에 따라서는 심볼명이 "dbg"가 아니라 "_dbg"가 될 경우도 있지만 크게 다르지는 않다.

한편, C++ 컴파일러로 함수를 컴파일한 경우 함수가 속한 이름공간(namespace) 정보

또는 함수 인자의 자료형 정보가 심볼명에 포함되어 나타난다.

  

   

   

[ C++에서 C함수 호출하기 ]

▶ dbg.c를 C컴파일러로 컴파일한 후 C++로 작성된 함수(sample.cpp)에서 호출해 보자.

① sample.cpp 작성

/****************

* sample.cpp

****************/

extern "C" void dbg(const char *s);

int main() {

dbg("foo");

return 0;

}

② sample.cpp 컴파일, 링크하면 정상적으로 실행 파일이 생성/실행

이상 없이 C++함수(sample.cpp)에서 C함수(dbg)를 호출 하였다.

☞ sample.cpp의 『extern "C"』가 포인트다.

   

▶ 『extern "C"』의 유무에 따라 sample.o의 변화.

① 『extern "C"』가 있을 경우.

② 『extern "C"』가 없을 경우.

☞ ②에서 "sample.o"는 "_z3dbgPKc"라는 심볼을 참조하지만,

dbg.o에 포함된 심볼은 단지 "dbg""_z3dbgPKc"는 심볼은 어디에도 없다.

   

▷ 따라서 extern "C"를 넣지 않으면 다음과 같이 링크할 때 실패하게 된다.

이와 같이 C++에서 C함수를 호출할 때 extern "C"가 중요한 역할을 한다.

   

〔주의〕 extern "C"를 붙이면 인자의 자료형 일치 여부를 검사하지 않는다.

① sample.cpp의 dbg자료형 변경.

/****************

* sample.cpp

****************/

//extern "C" void dbg(const char *s);

extern "C" void dbg(int i); // 잘못된 함수 원형 선언

int main() {

dbg("4444");

return 0;

}

② 컴파일, 링크, 실행.

☞ 정상적으로 링크는 되지만 생성된 실행 파일은 정상적으로 실행 되지 않는다.

   

〔왜! 링크는 성공한 것일까?〕

sample.o가 참조하고 있는 것은 어디까지나 자료형 정보를 포함하지 않은 심볼 "dbg"로,

dbg.o에 포함되어 있는 심볼명과 일치하기 때문이다.

"링크에 성공하면 잘못된 자료형으로 함수가 호출되지 않는다."라고 생각하기 쉽지만,

C++라도 extern "C"를 사용한 부분인 경우에 한해서는 그렇지 않다.

이는 컴파일러에서 자료형을 검사하는 안전성 수준이 C언어 수준으로 저하되기 때문이다.

   

▷ 이러한 문제를 미연에 방지하려면 C언어 측의 헤더 파일을

C++에서도 이용할 수 있도록 준비해 두어야 한다.

예를 들면, 아래의 dbg.h와 같은 헤더 파일을 작성하는 것이 바람직하다.

/**************

* dbg.h

**************/

#ifdef __cplusplus

extern "C" {

#endif

void dbg(const char *s);

#ifdef __cplusplus

}

#endif

  

   

   

[ C에서 C++함수 호출하기 ]

▷ 이번에는 반대로 C에서 C++ 함수를 호출해 보도록 하자.

『최대공약수를 구하는 함수』를 Boost C++ Library를 이용해

C++로 간단히 구현하고, 그 함수를 C에서 호출해 보자.

   

〔중요한 점 두가지〕
º C++함수를 C 링키지(extern "C")로 컴파일.

º 링크는 gcc가 아닌, g++로 수행.

   

▷ C++측 구현

/***********

* gcd.h

***********/

#ifdef __cplusplus

extern "C"

#endif

int gcd (int v1, int v2);

/****************

* gcd.cpp

****************/

#include <boost/math/common_factor.hpp>

extern "C" {

int gcd (int v1, int v2) {

return boost::math::gcd(v1, v2);

}

}

   

▷ C++함수를 호출하는 C프로그램은 특별히 달라진 것이 없다.

/*************

* sample.c

*************/

#include <stdio.h>

#include "gcd.h"

   

int main() {

printf("%d와 %d의 최대공약수는 %d(이)다.\n", 14, 35, gcd(14,35));

return 0;

}

   

▶ 컴파일, 실행.

  

   

 

〔 주의 사항 〕

C++함수는 C함수로 예외 처리를 넘겨서는 안됨.

C언어에서 호출되는 C++함수를 작성할 때는 C++의 예외가 C함수로 도달하지 않도록 주의.

C++의 예외가 C언어로 작성된 함수에 도달된 경우의 함수 동작에 대해 C++표준 규격에는

명확히 정의되어 있지 않다. 예를 들면 GCC에서는 프로세스가 이상 종료되는 현상이 나타남.

C++ 함수에서 명시적인 예외를 throw하지 않도록 신경 쓰는 것은 물론,

▷ 다음과 같은 점도 충분히 주의해야 한다.

º new 연산자가 std::bad_alloc 예외를 throw할 가능성

º std::vector의 멤버 함수 at에서 std::out_of_range 예외를 throw할 가능성

   

함수 포인터를 다루는 C함수에 주의.

C함수 내부에서 C++의 예외가 발생하는 것을 막을 수 없는 경우가 있다.

다음과 같이 표준 C의 qsort함수를 C++에서 사용한 경우를 생각해 보자.

/**************

* sample2.cpp

**************/

#include <cstdlib>

using namespace std;

   

// qsort 비교 함수

int compar(const void *, const void *) {

throw -1;

}

   

int main() {

int array[] = {3, 2, 1};

try {

qsort(array, 3, sizeof(int), compar);

} catch(...) {

return -1;

}

return 0;

}

☞ qsort 함수에 인자로 넘긴 비교함수 compar가 예외를 throw하기 때문에

qsort함수내부에서 C++의 예외가 발생하게 된다. GCC를 사용할 경우에

이와 같은 경우라도 프로세스가 이상 종료되지 않기 위해서는 아래와 같이

qsort 함수를 "-fexceptions"옵션을 지정해서 컴파일 해야 한다.

% gcc -fexceptions qsrot.c

   

일반적으로 함수 포인터를 다루는 C함수는 -fexceptions옵션을 지정해서 컴파일해야

오류가 발생하지 않는다. glibc의 Makefile을 보면 이러한 함수(qsort, bsearch 등)는 이 옵션을

지정해서 컴파일 하도록 되어 있다.

 

Posted by devanix
2011. 7. 4. 02:04

♧ 사실 ar 명령으로 생성한 아카이브는 정적 라이브러리에 국한되지 않고

tar(1) 등과 마찬가지로 범용적인 비압축 아카이브로 사용할 수 있다.

그러나 일반적으로는 정적 라이브러리를 다루기 위해 사용된다.

   

   

▶ 아카이브를 생성할 경우 :: ar rcus [라이브러리 이름] [오브젝트 파일들]

r 옵션 :: 새로운 오브젝트 파일이면 추가, 기존 파일이면 치환함.

c 옵션 :: libhoge.a 파일이 존재하지 않아도 경고 메시지를 출력하지 않음.

u 옵션 :: 오브젝트 파일의 타임스탬프를 비교해 새로운 파일일 경우에만 치환함.

s 옵션 :: ranlib(1)과 마찬가지로 아카이브 인덱스를 생성.

→ 아카이브 인덱스를 생성하지 않으면 링크 속도가 느려지고, 시스템 환경에 따라서는 에러가 발생.

(※ 아카이브 인덱스는 nm -s로 조회할 수 있다.)

   

▶ 기존 아카이브 파일에 오브젝트 제거 :: ar ds [라이브러리 이름] [오브젝트 파일들]

d 옵션 :: 아카이브 모듈을 삭제

→ 삭제할 파일이 없다면 아카이브를 건들지 않음.

   

▶ 아카이브의 내용을 조회 :: ar tv [라이브러리 이름]

t 옵션 :: 아카이브에 있는 파일 리스트 출력

v 옵션 :: 자세한 내용을 보여주는 verbose 모드.

→ 이 옵션을 이용하면 파일크기나 갱신시각 등의 정보도 출력.

   

▶ 아카이브에서 파일을 추출하려면 :: ar xv [라이브러리 이름]

x 옵션 :: 아카이브에서 오브젝트 파일 추출

→ v옵션을 지정하면 추출한 파일에 대한 파일명 출력.

   

 

▷ ar 명령은 정적 라이브러리를 생성할 때뿐만 아니라,

정적 라이브러리의 기능을 부분적으로 변경하고자 할 경우에도 사용할 수 있다.

라이브러리가 모두 오픈 소스로 구성되어 있지 않은 이유 등으로

정적 라이브러리를 처음부터 다시 작성할 수 없는 경우에 편리하게 이용될 수 있다.

   

그 밖에도 아카이브 냉의 이동, 제거 등 아카이브를 다루는 옵션이 더 있지만,

대부분의 경우 위에서 설명한 옵션만으로도 문제가 없을 것이다.

   

Posted by devanix
2011. 7. 4. 00:30

♧ strip은 오브젝트 파일에 있는 심볼을 삭제하는 툴.

일반적으로 빌드 완료한 실행파일 또는 라이브러리에서 불필요한 심볼을 제거하는데 사용.

   

[ strip 사용법 ]

strip 사용법은 간단하다.

기본적으로는 심볼을 제거하려는 오브젝트 파일을 인수로 지정하면 된다.

test라는 실행 파일에서 심볼을 제거하려는 경우 다음과 같이 실행.

   

▷ test.c 작성.

#include <stdio.h>

void test() { }

int main() {

return 0;

}

   

▷ gcc로 컴파일 하면 완성된 test에 심볼 정보가 포함된다.

이때 test는 8,327바이트 크기로 생성되었다.

   

▶ strip을 사용하여 심볼 제거.

심볼을 제거한 후 5,492바이트가 되었다.

C 소스코드를 보면 거의 비어 있는 프로그램인데 약 3,000바이트 정보가 줄었다.

이유는, 사실 test의 대부분이 /usr/lib/crt*.o라는 오브젝트 파일로 채워져 있기 때문.

   

-d옵션 :: 디버그용 정보(파일명 또는 행 번호 등)만을 제거하고

함수명 등의 일반 심볼은 남게 된다.

% gcc -g test.c -o test

% objdump -h test

...(중략)...

▷ strip -d로 삭제후 확인

   

▶ -R 옵션 :: 지정된 섹션을 제거하는 옵션.

stript -R .text program을 실행하면 프로그램의 텍스트 섹션(코드 부분)이

모두 제거되어 전혀 작동하지 않게 된다. 또한 .o 또는 .a 파일에 strip 명령을 실행하면

사실상 다른 오브젝트 파일과 링크를 할 수 없게 된다.

이는 링커가 심볼에 의존하기 때문이다.

그러므로 .o.a 파일에서 심볼을 제거하지 않도록 한다.

   

▷ 한편, 완성 제품으로 판매한 버전의 바이너리는 strip하고

개발자는 디버그 정보가 포함된 바이너리를 남겨 둔다면,

사용자 환경에서 생성된 코어 파일을 개발 환경에서 디버깅할 수 있게 된다.

   

   

[ strip 구조 ]

strip은 BSD 라이브러리를 이용해 제작된 툴로, BFD API를 이용해서 오브젝트 파일을 조작.

GNU Binutils의 소스코드를 보면 objcopy와 동일한 코드로 이루어져 있음을 알 수 있다.

실제로 objcopy 명령에 -strip-*옵션을 사용하면 strip과 같은 기능을 수행한다.

   

   

[ 정리 ]

디스크 용량이 풍부한 PC에서는 실행 파일의 크기를 줄일 수 있다는 장점을 느끼기 어렵지만,

사용할 수 있는 디스크나 메모리 용량이 제한적인 시스템 환경에서 프로그램을 설치할 경우

또는 프로그램을 네트워크로 전송해서 실행해야 할 경우에는 상당히 유용한 툴.

Posted by devanix
2011. 7. 3. 23:46

addr2line은 디버그 정보를 이용해서 주소로부터 파일명과 행 번호를 얻는다.

이 때문에 프로그램은 미리 디버그 정보를 포함(gcc -g) 하도록 컴파일 해야 한다.

   

▶ test.c 작성.

#include <stdio.h>

void func() { }

    int main() {

    printf ("%p\n", &func);

    return 0;

}

이를 디버그 정보를 포함하도록 컴파일/ 실행.

   

▷ addr2line을 사용하여 주소에 해당하는 파일명과 행 번호를 출력.

[ -e filename ] :: 실행 파일 지정

   

▷ 함수명과 함께 출력.

[ -f | --functions ] :: 함수명 출력

   

▷ 표준입력으로부터 주소를 addr2line 으로 넘길 수도 있다.

이는 여러 주소를 모아서 처리하고자 할 때 편리하다.

  

Posted by devanix
2011. 7. 3. 23:11

♧ c++ 컴파일러는 단 하나의 이름을 갖는 심볼이 되도록

name mangling이라는 과정을 수행한다.

여기서는 명령행에서 C++의 심볼을 demangle하는 방법을 소개한다.

   

▷ C++ 오브젝트 파일을 nm명령으로 확인하면 기본적으로 심볼명은

name mangling된 읽기 어려운 형식으로 출력 된다.

   

c++filt를 이용해여 demangle.

   

nm에 --demangle 옵션을 사용하여 demangle.

  

Posted by devanix