실행파일 크기 줄이기

(무선)네트워크 2009. 9. 8. 09:09 Posted by 알 수 없는 사용자

바이러스에 대하여 공부를 하다보면 작은크기의 바이너리가 시스템에 엄청난 타격을 주는것에 대해 놀라지 않을 수 없다.

바이러스를 통해 영감을 얻어 실행파일의 크기를 줄이는 몇가지 방법에 대해서 알아 보자

(실제 바이러스는 LoadLibrary, GetProcAddress를 통하여 필요한 API만 사용하면서 크기를 줄이는 방법을 사용하기도 한다.)

실행파일의 크기를 줄일 수 있는 방법으로는 #pragma 라는 키워드를 사용하면 된다.

#pragma는 define 이나 include와 같이 #으로 시작하는 전처리구문(precompiler)의 하나이다.

컴파일러에 종속적인 구문이라 컴파일러가 변경되었을 경우 제대로된 동작을 보장하지 못하므로 프로젝트 진행중에 서로 다른 컴파일러를 사용한다면 사용하지 않음이 바람직 하겠으나,

Visual Studio로 코딩을 하고 cl컴파일러를 사용하는 한 그런 걱정은 하지 않아도 된다.

실행파일의 크기를 줄일 수 있는 3가지 방법에는 EntryPoint를 바꾸는 방법, FileAlignMent를 바꾸는 방법, SectionMerge가 있다.

1. Entry Point

보통 프로그래밍을 하다보면 맨처음 시작하는 부분이 있다. 

C언어에서는 main( ... ) 함수, 윈도우즈  Programming 에서는 WinMain( ... ) 에 해당된다.

이를 EntryPoint라고 한다.

많은 문서들이 윈도우즈 어플리케이션의 진입 함수를 다음과 같은 함수로 고정된 것으로 설명하고 있다.

int WINAPI WinMain(
        HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        PSTR szCmdLine,
        int iCmdShow)

하지만 windows.h를 include하여 WinMain()을 이용하게 된다면 7KB정도의 추가적인 코드가 덧붙여 지게 된다.

이렇게 비 생산적인 코드를 제거하려면 진입함수를 변경하면 되는데 linker에게 어떤 진입함수를 쓰는지 알려주는 방법으로 "#pragma comment" directive를 사용하면 된다.

#pragma의 사용법에 대해 잠시 보자면

#pragma comment()는

#pragma comment( comment-type, ["comment string"] )

의 형태를 가지게 된다.

[] 안의 구문은 comment-type에 따라 필요할 경우 사용하는 것이다.

comment type에는 compiler, exestr, lib, linker, user 등이 올 수 있다.

linker 를 사용하면 프로젝트를 console application인지 win32 application인지 명시해줄 수 있다.

#pragma comment( linker, "/subsystem:windows" )
#pragma comment( linker, "/subsystem:console" )

또한 섹션의 설정을 할 수 있다.

#pragme comment( linker, "SECTION:.SHAREDATA,RWS" )

이 중 가장 대표적인 사용법은 명시적인 라이브러리의 링크이다.

#pragma comment(lib, "xxxx.lib")

이렇게 작성하여 주면 VisualStudio에서 별도 설정없이 Library를 사용할 수 있다.

다시 Entry Point를 설정하는 예로 가보자. 

#include <windows.h>
#pragma comment (linker, "/ENTRY:main") //main부터 시작함
 
int main()
{
    MessageBox(NULL, TEXT("test"), TEXT("test caption"), MB_OK);
    return 0;
}

이렇게 하면( #pragma comment (linker, "/ENTRY:main")   ) EntryPoint가 main으로 변경할 수 있다.

2. File Alignment

PE파일구조에서는 Section Alignment, File Alignment라는 용어를 사용하게 된다.
 
각 크기에 따라 파일상의 위치와 메모리상의 위치가 달라지게 되고 이에 따라 Pedding이 생기게 된다.

VC++ 6.0에서 default file alignment는 0x1000 바이트이다.
 
이 옵션에서는 각 section 사이에 의미없는 빈 공간을 많이 만들어 낸다. 이것은 불필요한 낭비가 아닐 수 없다.
 
만약 우리가 만드는 어플리케이션이 3개의 section(.text, data, rdata)으로 이루어져 있다면
 
최소한 0x1000 * 3 = 0x3000 = 12288 바이트나 차지한다는 의미이다.
 
0x1000 바이트 미만의 코드로 짜여진 프로그램이라면 이 역시 많은 부분이 낭비이다.
 
linker에게 FILEALIGN 옵션을 주어서 file alignment를 변경할 수 있다.

#pragma comment(linker,"/ENTRY:main /FILEALIGN:0x200 /IGNORE:4078")

위 옵션은 File Alignment를 0x200으로 주었는데 상황에 따라 적당한 겂으로 설정하면 된다.

IGNORE 옵션은 컴파일러가 4078번 에러를 반환하면 무시하라는 의미이다.
 
이렇게 PEDDING의 크기가 줄어들기 때문에 최종 실행파일 사이즈를 줄일 수 있다.

3. Section Merging

PE파일구조서도 설명되어 있지만, PE 파일 내에서 각 section에는 header, info등의 정보가 추가적으로 따라 붙는다.
 
만약 section들을 하나로 merge한다면 이런 추가적인 공간들을 제거할 수 있을 것이다.
 
그러나 section들을 merge할 때는 section의 내용을 정확히 파악해야 한다.

 section   Description 
  .text   코드 텍스트 영역
  .data   문자열 등이 저장되는 데이타 섹션
  .rsrc   image import descriptor 등이 저장되는 리소스 데이타 영역

그럼 모든 section들을 하나의 section으로 합쳐 보자.

주의할 것은 코드 텍스트 영역은 read-only 영역이고, .data나 .rdata는 쓰기 권한이 필요한 영역이므로 무턱대고 합치면 안되고 액세스 권한을 설정해 줘야 한다.

#pragma comment(linker,"/ENTRY:main /FILEALIGN:0x200/SECTION:.text,EWR /IGNORE:4078")

 설명  속성 
 Executable  E
 Writeable  W
 Readalble  R

이렇게 하면 코드 영역이 executable, write, read등이 가능해 지도록 하는 것이다.
 
실제로 section을 머지하는 옵션은 다음과 같다.

#pragma comment(linker,"/ENTRY:main /FILEALIGN:0x200
    /MERGE:.data=.text      //.data가 .text로 합쳐짐
    /MERGE:.rdata=.text     //.rdata가 .text로 합쳐짐
    /SECTION:.text,EWR /IGNORE:4078")

이런 방법 외에도 어셈블리어로 직접 하드코딩하는 방법도 있다.

어셈블리어로 코딩을 하게 되면 코드 최적화 및 컴파일시 붙는 군더더기 코드가 그나마 줄기 때문에 어셈블리어로 직접 하드코딩을 하는 방법도 있다.