최근 개인적으로 리눅스 환경에서 C/C++을 혼합해서 쓸 일이 생겼는데, 그때 extern "C"를 사용하면서 알게 된 점들을 작성해 보려고 합니다. 해당 프로젝트는 'https://github.com/skyclad0x7b7/MiniHook' 요놈인데, LD_PRELOAD라는 환경변수를 이용하여 간단하게 함수들을 후킹하여 프로세스가 어떤 행위를 했는지 로깅하는 툴입니다. 윈도우 악성코드 자동 분석 시스템은 많이 만져 봤지만 리눅스는 한번도 해본적이 없었기 때문에 윈도우에서와 비슷한 방식으로 자동 분석이 가능하지 않을까 하여 만들어 보게 되었습니다.



 우선 extern "C"란 무엇인가에 대해 알아보아야 합니다. extern "C"라는 키워드는 C++ 소스에서 선언한 전역 변수나 함수를 C에서 사용해야 할 경우에 쓰입니다. 그 이유는 함수명이나 특정 전역변수명을 '심볼'로 저장하는 방식이 다르기 때문입니다.


1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
void test()
{
    printf("Hello World!\n");
}
 
int main()
{
    test();
    return 0;
}
cs


 위의 간단한 소스코드를 예로 들겠습니다. test라는 함수 안에서 Hello World를 출력하고 종료하는 프로그램입니다. 위 소스코드를 각각 gcc와 g++로 컴파일하고, readelf 명령을 이용해 만들어진 바이너리 안의 심볼을 찾아보았습니다.




 gcc로 컴파일한 경우 test라는 함수명이 그대로 출력되지만, g++로 컴파일한 경우 _Z4testv 라는 복잡한 이름으로 변경된 것을 볼 수 있습니다. 이는 C와 C++의 함수 특성에서 차이가 발생하기 때문입니다. C++에는 '함수 오버로딩'이라고 하는 기능이 있는데, 이는 같은 함수명이라고 하더라도 전달받는 타입이 달라진다면 새 함수로 선언 및 사용이 가능한 특징입니다. int func(int a)라는 함수와 int func(double a) 라는 함수를 둘 다 선언하고 정의하더라도 문제가 없게 된다는 것입니다. 하지만 이렇게 했을 시, func라는 함수명만으로 해당 함수들을 구별하는 것이 불가능해집니다. 그래서 C++ 컴파일러들은 각 컴파일러마다 자신들만의 규칙으로 함수 이름을 변경합니다. 이것을 '네임 맹글링(Name Mangling)'이라고 합니다.



각 컴파일러별 네임 맹글링 규칙

Compilervoid h(int)void h(int, char)void h(void)
Intel C++ 8.0 for Linux_Z1hi_Z1hic_Z1hv
HP aC++ A.05.55 IA-64
IAR EWARM C++ 5.4 ARM
GCC 3.x and higher
Clang 1.x and higher[1]
IAR EWARM C++ 7.4 ARM_Z<number>hi_Z<number>hic_Z<number>hv
GCC 2.9xh__Fih__Fich__Fv
HP aC++ A.03.45 PA-RISC
Microsoft Visual C++ v6-v10 (mangling details)?h@@YAXH@Z?h@@YAXHD@Z?h@@YAXXZ
Digital Mars C++
Borland C++ v3.1@h$qi@h$qizc@h$qv
OpenVMS C++ V6.5 (ARM mode)H__XIH__XICH__XV
OpenVMS C++ V6.5 (ANSI mode)CXX$__7H__FIC26CDH77CXX$__7H__FV2CB06E8
OpenVMS C++ X7.1 IA-64CXX$_Z1HI2DSQ26ACXX$_Z1HIC2NP3LI4CXX$_Z1HV0BCA19V
SunPro CC__1cBh6Fi_v___1cBh6Fic_v___1cBh6F_v_
Tru64 C++ V6.5 (ARM mode)h__Xih__Xich__Xv
Tru64 C++ V6.5 (ANSI mode)__7h__Fi__7h__Fic__7h__Fv
Watcom C++ 10.6W?h$n(i)vW?h$n(ia)vW?h$n()v

(출처 : https://en.wikipedia.org/wiki/Name_mangling)


 이렇게 네임 맹글링이 되는 대상은 함수만 있는 것이 아니고, 심볼을 통해 접근해야 하는 전역변수도 똑같이 적용됩니다. 여기서 몇번 테스트를 거치며 깨달은 점이 있는데, C++ 소스에서 선언한 전역변수라고 하더라도 모든 전역변수가 다 네임 맹글링이 되지는 않는다는 점입니다. 


test.h

1
2
3
4
5
6
#include <string>
#include <vector>
 
extern int TestInt;
extern std::string TestString;
extern std::vector<std::string> TestVector;
cs


test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string>
#include <vector>
#include "test.h"
 
int TestInt;
std::string TestString;
std::vector<std::string> TestVector;
 
void test()
{
    printf("Hello World!\n");
}
 
int main()
{
    TestInt = 1;
    TestString = "Test";
    TestVector.push_back("Test");
    test();
    return 0;
}
cs


 test.h에서 선언한 전역변수를 test.cpp에서 가져다가 정의하는 소스코드입니다. 위 소스코드를 컴파일한 후 마찬가지로 readelf로 심볼을 확인해보겠습니다.



 실제로 네임 맹글링이 적용된 것은 std::string 타입인 TestString과 std::vector<std::string> 타입인 TestVector뿐이라는 것을 알 수 있습니다. 단순 int형이었던 TestInt의 경우에는 네임 맹글링이 적용되지 않았습니다. 여기서 예측 가능한 것은 C에서도 사용하는 일반적인 타입들(함수 포인터, 기본 타입 변수들)의 전역변수의 경우에는 네임 맹글링이 적용되지 않는다는 것입니다.



(https://stackoverflow.com/questions/17064471/g-name-mangling-of-global-const-variables)

 

 또 이걸 조사하다가 스택오버플로우에서 재밌는 글을 하나 발견했는데, 바로 static 전역변수의 경우는 일반적인 C 타입의 변수라고 할지라도 네임 맹글링을 진행한다고 합니다. 사실 잘 생각해 보면 당연한 것인데, static 전역변수라고 함은 다른 소스코드에서는 접근할 수 없는 변수이므로 다른 소스코드 안에 같은 이름을 가진 다른 변수가 있을 수 있기 때문에 충돌을 피하기 위해 맹글링을 하는 것이 맞습니다. 또한 저 답변의 implicitly static 이라는 말이 뭔가 해서 다시 조사를 해 봤습니다.



(https://stackoverflow.com/questions/3709207/c-semantics-of-static-const-vs-const)


 결론만 말하면, C++에서 전역변수에 static const를 붙이는 것과 const만 붙이는 것은 동일한 동작을 합니다. 즉 const로 선언한 전역변수의 경우에는 extern 키워드를 붙여주지 않는 이상 static 변수로 취급되기 때문에 네임 맹글링이 진행됩니다. 여기까지 정리하면 C++에서 전역변수로 선언한 일반 타입의 변수들의 경우, const 혹은 static 변수에 대해서만 네임 맹글링이 진행된다고 볼 수 있겠습니다.




 어쨌든 네임 맹글링이 되는 조건은 이렇게 된다고 치고, 다시 돌아와서 이런 이유로 네임 맹글링이 일어나기 때문에 C++ 헤더 안에서 test라고 선언한 함수를 C에서 그대로 가져다 쓰려고 하면 네임 맹글링에 의해 변환된 심볼을 알 방도가 없기 때문에 에러가 발생할 수밖에 없습니다. 이를 가능하도록 해주는 것이 extern "C" 키워드입니다. extern "C"를 이용하여 변수나 함수를 선언할 경우 네임 맹글링이 진행되지 않습니다.


test.h

1
2
3
4
5
6
7
8
#include <string>
#include <vector>
 
extern "C" {
    extern int TestInt;
    extern std::string TestString;
    extern std::vector<std::string> TestVector;
}
cs


 위에서 사용한 test.h 헤더 파일에서 변수 선언부를 extern "C"로 감싸준 후 컴파일해보면



 위와 같이 변수명들이 맹글링되지 않고 원본 그대로 출력되는 것을 볼 수 있습니다. 빌드 혹은 실행 시 undefined symbol 에러와 함깨 네임 맹글링이 진행된 심볼이 나타난다면 대부분은 이 부분 문제일 것이므로 앞으로 문제 해결할때 참고하여 고쳐나가야겠습니다.





블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag

댓글을 달아 주세요