iOS逆向04 -- 编译过程

编译器的组成部分

传统的编译器通常分为三个部分,分别为:前端(frontEnd)优化器(Optimizer)后端(backEnd),在编译过程中,各自执行不同的功能:

  • 前端(frontEnd)主要负责词法分析,语法分析和语义分析,将源代码转化为抽象语法树(AST),最后生成中间代码;
  • 优化器(Optimizer)则是在前端的基础上,对生成的中间代码进行优化;
  • 后端(backEnd)则是将已经优化的中间代码,根据不同平台架构转化为各自平台的机器代码;

编译器的种类

GCC
  • GCC(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器,GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言,GCC 快速演进,变得可处理 C++、Fortran、Pascal、Objective-C、Java, 以及 Ada 等他语言;
LLVM
  • LLVM是一个完整的编译器(compiler)框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本;
  • 在理解LLVM时,我们可以认为它包括了一个狭义的LLVM和一个广义的LLVM;
  • 广义的LLVM其实就是指整个LLVM编译器架构,包括了前端、后端、优化器、众多的库函数以及其他的模块;
  • 狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库;
Clang
  • Clang是LLVM编译系统的前端,是GCC的替代品,其可以看成是LLVM的子集,相比于GCC编译器Clang功能更加强大;
  • 其速度快,占用内存小,诊断信息可读性强,兼容性好,Clang有静态分析而GCC没有,Clang使用BSD许可证,GCC使用GPL许可证;
  • 下面用一张图来表示Clang与LLVM之间的关系:
Snip20210112_43.png
  • 从图中可以看出Clang其实大致上可以看成是LLVM编译器架构的前端,主要处理一些和具体机器无关的针对语言的分析操作;编译器的优化器部分和后端部分其实就是我们之前谈到的LLVM后端(狭义的LLVM),而整体的Compiler架构就是LLVM架构;

iOS编译过程

  • Xcode的默认使用的编译器是 clang,clang首先会对 Objective-C 代码做预处理,分析检查,然后将其转换为低级的类汇编代码;
  • 下面通过具体的代码来演示iOS的编译过程:
第一步:预处理(preprocessor)
  • C语言代码如下所示:
#import 

#define DEBUG 1

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //insert code ...
        #ifdef DEBUG
          printf("hello debug\n");
        #else
          printf("hello world\n");
        #endif
          NSLog(@"Hello, World!");
    }
    return 0;
}
  • 终端cd到指定文件路径,然后执行xcrun clang -E main.m,最后终端输出的代码如下所示:
int main(int argc, const char * argv[]) {
    @autoreleasepool {

          printf("hello debug\n");

          NSLog(@"Hello, World!");
    }
    return 0;
}
  • 可以看到,在代码预处理的时候,注释被删除,条件编译指令被处理
  • 代码预处理完成之后的具体编译流程如下图所示:
Snip20210112_44.png
第二步:词法分析
  • 词法分析:通过词法分析器读入源文件的字符流,然后将他们组织成有意义的词素(lexeme)序列,对于每个词素,词法分析器会生成对应的词法单元(token)作为输出,测试代码如下:
#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        int b = 20;
        int c = a + b;
        NSLog(@" c = %d",c);
    }
    return 0;
}
  • 终端cd到指定文件路径,执行词法分析命令:xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m 最后终端输出的代码如下所示:
annot_module_include '#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        int b'      Loc=
int 'int'    [StartOfLine]  Loc=
identifier 'main'    [LeadingSpace] Loc=
l_paren '('     Loc=
int 'int'       Loc=
identifier 'argc'    [LeadingSpace] Loc=
comma ','       Loc=
const 'const'    [LeadingSpace] Loc=
char 'char'  [LeadingSpace] Loc=
star '*'     [LeadingSpace] Loc=
identifier 'argv'    [LeadingSpace] Loc=
l_square '['        Loc=
r_square ']'        Loc=
r_paren ')'     Loc=
l_brace '{'  [LeadingSpace] Loc=
at '@'   [StartOfLine] [LeadingSpace]   Loc=
identifier 'autoreleasepool'        Loc=
l_brace '{'  [LeadingSpace] Loc=
int 'int'    [StartOfLine] [LeadingSpace]   Loc=
identifier 'a'   [LeadingSpace] Loc=
equal '='    [LeadingSpace] Loc=
numeric_constant '10'    [LeadingSpace] Loc=
semi ';'        Loc=
int 'int'    [StartOfLine] [LeadingSpace]   Loc=
identifier 'b'   [LeadingSpace] Loc=
equal '='    [LeadingSpace] Loc=
numeric_constant '20'    [LeadingSpace] Loc=
semi ';'        Loc=
int 'int'    [StartOfLine] [LeadingSpace]   Loc=
identifier 'c'   [LeadingSpace] Loc=
equal '='    [LeadingSpace] Loc=
identifier 'a'   [LeadingSpace] Loc=
plus '+'     [LeadingSpace] Loc=
identifier 'b'   [LeadingSpace] Loc=
semi ';'        Loc=
identifier 'NSLog'   [StartOfLine] [LeadingSpace]   Loc=
l_paren '('     Loc=
at '@'      Loc=
string_literal '" c = %d"'      Loc=
comma ','       Loc=
identifier 'c'      Loc=
r_paren ')'     Loc=
semi ';'        Loc=
r_brace '}'  [StartOfLine] [LeadingSpace]   Loc=
return 'return'  [StartOfLine] [LeadingSpace]   Loc=
numeric_constant '0'     [LeadingSpace] Loc=
semi ';'        Loc=
r_brace '}'  [StartOfLine]  Loc=
eof ''      Loc=
  • 看到词法分析器将代码中的关键字,操作符,变量,分号,括号全部分割开来成为独立的词法单元
第三步:语法分析
  • 词法分析获取的词法单元(Token)流,会被解析成一棵抽象语法树(abstract syntax tree - AST),且会对语法树进行代码静态分析,校验语法是否错误;
  • 终端执行语法分析命令:clang -Xclang -ast-dump -fsyntax-only main.m,终端输出结果如下所示:
Snip20210112_45.png
第四步:生成中间代码文件
  • 终端执行命令:clang -O3 -S -emit-llvm main.m -o main.ll,本地生成一个main.ll文件,用Xcode打开其内容如下:
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"

%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }

@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [8 x i8] c" c = %d\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i32 0, i32 0), i64 7 }, section "__DATA,__cfstring", align 8

; Function Attrs: ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
  %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 30)
  tail call void @llvm.objc.autoreleasePoolPop(i8* %3)
  ret i32 0
}

; Function Attrs: nounwind
declare i8* @llvm.objc.autoreleasePoolPush() #1

declare void @NSLog(i8*, ...) local_unnamed_addr #2

; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(i8*) #1

attributes #0 = { ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "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" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind }
attributes #2 = { "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "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" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 10, i32 15]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 11.0.0 (clang-1100.0.33.17)"}
第五步:优化器(Optimizer) 优化中间代码
  • 全局变量优化、循环优化、尾递归优化等,如果开启了 bitcode 苹果会做进一步的优化;
第六步:生成汇编文件
  • 终端执行命令:clang -S -fobjc-arc main.m -o main.s,本地生成一个main.s文件,用Xcode打开其内容如下:
    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 15    sdk_version 10, 15
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    subq    $48, %rsp
    movl    $0, -4(%rbp)
    movl    %edi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    callq   _objc_autoreleasePoolPush
    leaq    L__unnamed_cfstring_(%rip), %rsi
    movl    $10, -20(%rbp)
    movl    $20, -24(%rbp)
    movl    -20(%rbp), %edi
    addl    -24(%rbp), %edi
    movl    %edi, -28(%rbp)
    movl    -28(%rbp), %edi
    movl    %edi, -32(%rbp)         ## 4-byte Spill
    movq    %rsi, %rdi
    movl    -32(%rbp), %esi         ## 4-byte Reload
    movq    %rax, -40(%rbp)         ## 8-byte Spill
    movb    $0, %al
    callq   _NSLog
    movq    -40(%rbp), %rdi         ## 8-byte Reload
    callq   _objc_autoreleasePoolPop
    xorl    %eax, %eax
    addq    $48, %rsp
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  " c = %d"

    .section    __DATA,__cfstring
    .p2align    3               ## @_unnamed_cfstring_
L__unnamed_cfstring_:
    .quad   ___CFConstantStringClassReference
    .long   1992                    ## 0x7c8
    .space  4
    .quad   L_.str
    .quad   7                       ## 0x7

    .section    __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
    .long   0
    .long   64

.subsections_via_symbols
第七步:生成目标文件(二进制文件) main.o
  • 终端执行命令:xcrun clang -fmodules -c main.m -o main.o,本地生成一个main.o文件;
第八步:链接器 链接多个目标文件最终生成一个可执行的Mach-O文件
  • 链接器把编译生成的所有 .o 文件和(dylib、a、tbd)文件结合,生成一个Mach-O文件(可执行文件);
  • 使用终端命令:xcrun clang main.o -o main
  • 在编译时,链接器的主要工作任务:
    • 符号绑定:完成变量名与其地址,函数名与其地址之间的绑定;
    • Mach-O文件中的主要内容是数据和代码,数据就是全局变量,代码就是函数, 全局变量和函数都存储在指定的位置,计算机指令通过符号去操作数据和调用函数,首先必须要进行符号的绑定,才能根据符号去操作数据和调用函数;
    • 静态库的链接:将静态库文件直接打包进入Mach-O文件中;
    • 生成的多个Mach-O文件最终合成一个Mach-O文件,项目中各个文件之间的函数存在相互调用,也就是说存在相互依赖,单个的Mach-O文件是无法执行成功的,因为当前文件中需要调用其他文件中的函数方法,最终由于找不到对应的函数而终止执行;所以我们需要把所有的Mach-O文件合并,最终生成一个可执行的Mach-O文件;
  • 对于Mach-O文件中的动态库,在编译期只是对其进行引用并没有加载,等到了App运行时,dyld会对Mach-O文件中所有动态库的引用进行扫描,逐一加载链接,但不会打包进Mach-O文件中,这是与静态库最大的区别;

编译多个文件 -- 实战演练

  • 首先创建一个C语言工程如下所示:
Snip20210118_13.png
  • 在终端依次输入:
    • xcrun clang -c main.m
    • xcrun clang -c YYPerson.m
  • 在本地会生成两个目标文件 main.o 与YYPerson.o;
Snip20210118_14.png
  • 将多个编译之后的目标文件通过链接器进行链接,生成a.out可执行文件;
  • 在终端中输入:xcrun clang main.o YYPerson.o -Wl,xcrun —show-sdk-path/System/Library/Frameworks/Foundation.framework/Foundation
Snip20210118_15.png
  • 在终端中输入以下命令进行查看:xcrun nm -nm a.out
Snip20210118_16.png
  • undefined 符号表示的是该文件类未定义,所以在目标文件和 Foundation framework 动态库做链接处理时,链接器会尝试解析所有的undefined 符号;
  • dylib 这种格式,表示是动态链接的,编译的时候不会被编译到执行文件中,在程序执行的时候才 link,这样就不用算到包大小里,而且不更新执行程序就能够更新库;

你可能感兴趣的:(iOS逆向04 -- 编译过程)