LogFunctionNames 모듈 개발 - puts함수 동적 삽입

컴퓨터공부/엘엘비엠 2016.07.22 20:26
크리에이티브 커먼즈 라이선스
Creative Commons License

우리는 저번 글에서 컴파일 할 때 함수 이름들을 로깅하는것을 테스트 해봤습니다

이번 글과 다음 글에서는 함수 이름 로깅 기능을 컴파일러에서 수행하는 것이 아닌, 해당 기능을 수행하는 코드를 컴파일되는 프로그램에 삽입합니다

하다가 중간결과를 확인하고 싶으실 때는 직접 빌드해서 돌려보시면 됩니다. 언제 빌드해서 확인해볼만 한지는 글 중간 중간 말씀드리겠습니다



위 사진은 우리가 가장 많이 사용할 각각 요소에 대한 포함관계를 설명합니다


좀더 쉬운 이해를 위해 아래 아이다 사진을 참고해주세요

우선 Module은 아이다에서 이해하기 쉽지는 않습니다. LLVM 에서 모듈이란 각 파일마다 구성됩니다.

예를 들어 이전 글에서 ./clang -o test test.c로 컴파일한 코드에 F.getParent()->getName() 을 로깅해보면 test.c가 출력됩니다


그 다음 Function은 이름 그대로 각각 함수마다 구성됩니다.


이 설명을 시작한 이유는, 다른건 모두 직관적으로 알 수 있지만 Basic Block에 대한 개념을 모르시는 분이 계실까봐입니다

BasicBlock은 일반적으로 분기와 분기 사이의 코드를 뜻합니다

즉 항상 선형으로 동작되는 코드들의 모음을 BasicBlock이라고 하고, IDA에서 그래프뷰로 볼때 각각 직사각형은 모두 Basic Block들입니다



이전 글에서 세팅한 뼈대 코드와 함수 이름을 로깅하는 코드에

errs() << "Current module name: [" << F.getParent()->getName() << "]\n";

위 코드를 추가하고 테스트 해보세요 


getParent()함수는 GlobalValue 클래스에서 정의합니다


Parent 멤버 선언

http://llvm.org/docs/doxygen/html/GlobalValue_8h_source.html#l00115

getParent 함수 구현체

http://llvm.org/docs/doxygen/html/GlobalValue_8h_source.html#l00361


즉 Parent는 Module * 형으로 선언돼있고 getParent()는 Parent를 리턴하기 때문에, getParent()의 리턴 형은 Module *입니다

그러면 Instruction이나 BasicBlock 클래스에서도 getParent가 Module을 리턴하느냐 하면 아닙니다. Instruction과 BasicBlock 클래스에서는 함수들이 모두 오버라이딩 돼있습니다

각각 레퍼런스를 확인해보면 맨위의 다이어그램에 맞게 parent를 리턴하는 것을 볼 수 있습니다



이제 첫 주제인 puts함수 삽입을 어떻게 하는가를 알아보겠습니다

그전에 우리는 LLVM IR파일에 좀더 익숙해져야 합니다


단순한 코드를 작성합니다

우리가 처음 쉘코드를 공부할 때 gcc -S write.c 와 같이 하면 write.S가 생성이 되고, 여긴 어셈블리어 코드가 들어가 있었습니다

llvm 에서도 똑같이 할 수 있는데, -emit-llvm이라는 옵션을 추가로 주면 어셈블리 파일이 아닌 LLVM IR파일을 생성해줍니다



IR파일을 읽으면 아래와 비슷하게 나옵니다

; ModuleID = 'puts.c'

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

target triple = "x86_64-unknown-linux-gnu"


@.str = private unnamed_addr constant [4 x i8] c"hi~\00", align 1


; Function Attrs: nounwind uwtable

define i32 @main() #0 {

entry:

  %call = call i32 (i8*, ...) bitcast (i32 (...)* @puts to i32 (i8*, ...)*)(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0))

  ret i32 0

}


declare i32 @puts(...) #1


attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }

attributes #1 = { "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }


!llvm.ident = !{!0}


!0 = !{!"clang version 3.8.0 (trunk 247122)"}

지금 제 시스템에 깔려있는 LLVM 버젼이 3.8.0이기 때문에 다른 버젼으로 컴파일하신다면 결과가 약간 다르게 나올 순 있지만 지금은 상관없습니다

IR 결과에서 함수만 추려보면

define i32 @main() #0 {

entry:

  %call = call i32 (i8*, ...) bitcast (i32 (...)* @puts to i32 (i8*, ...)*)(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0))

  ret i32 0

}


declare i32 @puts(...) #1

이렇게 main과 puts가 존재하는것을 알 수 있습니다

두 함수의 차이점은 하나는 define된 함수고 하나는 declare된 함수라는 것입니다

정확히 알진 못하더라도 느낌상 define된 함수는 현재 코드에서 함수가 정의된단걸 알 수 있고

declare된 함수는 다른 소스파일이나 라이브러리에 정의돼있는 함수란걸 알 수 있습니다

puts함수도 현재 코드에서 정의되는 함수가 아닌 라이브러리에서 정의된 함수죠


그러면 declare 될 함수를 어떻게 LLVM Pass에서 삽입할 수 있을까요?

이런 애매한걸 해결하기 가장 쉬운 방법은 IR파일을 LLVM API를 이용한 c++코드로 바꾸는 것입니다

저도 이 기능을 발견했을 때 매우 행복했는데, 어떤 기능인지 보여드리겠습니다


생성된 puts.cpp를 읽어서 puts 를 검색해보세요

막연하게만 느껴지던 puts함수 삽입을 이렇게 친절한 예제로 볼 수 있습니다

한줄한줄 완벽한 이해는 안되더라도 이렇게하면  위의 IR코드 처럼

declare i32 @puts(...) #1

이 한 줄을 IR파일에 자동으로 추가할 수 있겠죠


코드에 대한 약간의 설명을 드리자면

우선 mod는 현재 모듈을 가르키는 변수입니다. 우리의 경우엔 F.getParent()를 변수로 저장해서 사용하면 되겠죠

FuncTy_6은 잠시 후에 보도록 하고

GlobalValue::ExternalLinkage 는 http://llvm.org/docs/doxygen/html/classllvm_1_1GlobalValue.html#aedfa75f0c85c4aa85b257f066fbea57c 이 링크를 참고해주세요. 

그다음에 Attributes에 관련되어 보이는 코드들은 

attributes #1 = { "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }

이 부분을 만들어주는 코드입니다

그러려니 합시다..


위에서 스킵한 FuncTy_6은 여기있습니다

뭔진 모르겠지만 puts함수의 타입은 이런가봅니다

왜 isVarArg 가 true인진 모르겠지만 그러려니합시다..



        virtual bool runOnFunction(Function &F){

            errs() << "Current function name: [" << F.getName() << "]\n";

            errs() << "Current module name: [" << F.getParent()->getName() << "]\n";

            Module *mod = F.getParent();

            std::vector<Type*>FuncTy_6_args;

            FunctionType* FuncTy_6 = FunctionType::get(/*Result=*/IntegerType::get(mod->getContext(), 32),

                                                        /*Params=*/FuncTy_6_args,

                                                        /*isVarArg=*/true);

            Function* func_puts = mod->getFunction("puts");

            if (!func_puts) {

                func_puts = Function::Create(/*Type=*/FuncTy_6,

                                            /*Linkage=*/GlobalValue::ExternalLinkage,

                                            /*Name=*/"puts", mod); // (external, no body)

                func_puts->setCallingConv(CallingConv::C);

            }

            AttributeSet func_puts_PAL;

            {

                SmallVector<AttributeSet, 4> Attrs;

                AttributeSet PAS;

                {

                    AttrBuilder B;

                    PAS = AttributeSet::get(mod->getContext(), ~0U, B);

                }

                Attrs.push_back(PAS);

                func_puts_PAL = AttributeSet::get(mod->getContext(), Attrs);

            }

            func_puts->setAttributes(func_puts_PAL);

            return false;

        }

FuncTy_6과 puts를 삽입하는 코드를 그대로 옮겼습니다

임의로 추가한 부분은 4번째줄의 Module *mod = F.getParent(); 입니다


자 그럼 이제 테스트 해봅시다

빌드를 이미 완전히 해보셨다면 금방 빌드가 완료됩니다

컴퓨터 사양에따라 중간 중간 빌드하는게 무리다 싶으시면 제가 보여드리는 결과만 보고 넘어가셔도 괜찮습니다. 그래도 직접 확인해보는게 훨씬 와닿겠죠



⚡ root@vultr  ~/build/bin  clang -S -emit-llvm test.c -w

 ⚡ root@vultr  ~/build/bin  cat test.ll

; ModuleID = 'test.c'

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

target triple = "x86_64-unknown-linux-gnu"


@.str = private unnamed_addr constant [8 x i8] c"5+3=%d\0A\00", align 1


; Function Attrs: nounwind uwtable

define i32 @add(i32 %x, i32 %y) #0 {

entry:

  %x.addr = alloca i32, align 4

  %y.addr = alloca i32, align 4

  store i32 %x, i32* %x.addr, align 4

  store i32 %y, i32* %y.addr, align 4

  %0 = load i32, i32* %x.addr, align 4

  %1 = load i32, i32* %y.addr, align 4

  %add = add nsw i32 %0, %1

  ret i32 %add

}


; Function Attrs: nounwind uwtable

define i32 @main() #0 {

entry:

  %call = call i32 @add(i32 5, i32 3)

  %call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i32 0, i32 0), i32 %call)

  ret i32 0

}


declare i32 @printf(i8*, ...) #1


attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }

attributes #1 = { "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }


!llvm.ident = !{!0}


!0 = !{!"clang version 3.8.0 (trunk 247122)"}

 ⚡ root@vultr  ~/build/bin 

지금 이건 시스템에 설치돼있던 LLVM, 즉 puts함수를 자동으로 삽입하는 코드가 없는 LLVM입니다


 ✘ ⚡ root@vultr  ~/build/bin  ./clang -S -emit-llvm test.c -w

Current function name: [add]

Current module name: [test.c]

Current function name: [main]

Current module name: [test.c]

 ⚡ root@vultr  ~/build/bin  cat test.ll

; ModuleID = 'test.c'

source_filename = "test.c"

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

target triple = "x86_64-unknown-linux-gnu"


@.str = private unnamed_addr constant [8 x i8] c"5+3=%d\0A\00", align 1


; Function Attrs: nounwind uwtable

define i32 @add(i32 %x, i32 %y) #0 {

entry:

  %x.addr = alloca i32, align 4

  %y.addr = alloca i32, align 4

  store i32 %x, i32* %x.addr, align 4

  store i32 %y, i32* %y.addr, align 4

  %0 = load i32, i32* %x.addr, align 4

  %1 = load i32, i32* %y.addr, align 4

  %add = add nsw i32 %0, %1

  ret i32 %add

}


; Function Attrs: nounwind uwtable

define i32 @main() #0 {

entry:

  %call = call i32 @add(i32 5, i32 3)

  %call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i32 0, i32 0), i32 %call)

  ret i32 0

}


declare i32 @printf(i8*, ...) #1


declare i32 @puts(...)


attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

attributes #1 = { "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }


!llvm.ident = !{!0}


!0 = !{!"clang version 3.9.0 (trunk 275527) (https://github.com/cd80/UtilizedLLVM.git 2587b3df7a08ff644b1b31139619798829d7f95e)"}

 ⚡ root@vultr  ~/build/bin 

puts가 생겼습니다!


지금 우리가 어떤 지식을 기반으로 했다기 보다 일종의 예제코드를 보고 복사붙여넣기를 한것이기 떄문에 아직 우리가 LLVM에 대해서 안다! 라고는 할 수 없는 단계지만

이 방식으로도 충분히 우리가 원하는 어떤 기능이든지 다 구현할 수 있습니다



쓰는게 힘에부쳐서 한 글에 많은 내용을 담지는 못합니다

다음 글에서는 동적으로 삽입된 puts함수에 우리가 원하는 문자열을 인자로 전달하고 호출하는 코드를 작성한 뒤 이를 각 함수의 맨 처음 인스트럭션으로 삽입하는 과정을 다루겠습니다

직접 구글과 레퍼런스를 참고하면서 구현해보시고 저는 어떻게 구현하는지 보러오시는것도 좋은 공부가 될것 같습니다

신고

설정

트랙백

댓글


티스토리 툴바