链接器:符号是怎么绑定到地址上的 - 文章教程

链接器:符号是怎么绑定到地址上的

发布于 2021-03-05 字数 8800 浏览 1102 评论 0

链接器最主要的作用,就是将符号绑定到地址上。

iOS 开发使用的为什么是编译器

编译器把代码编译成机器码,然后直接在CPU上执行机器码。执行效率更高,运行速率能达到最快。
解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码(就是字节码),然后一句一句的执行目标代码。
优缺点对比:

  • 采用编译器生成机器码执行的好处是效率高,缺点是调试周期长
  • 解释器执行的好处是编写调试方便,缺点是执行效率低

苹果公司现在使用的编译器是LLVM,LLVM是编译器工具链技术的一个集合。而其中的 lld 项目,就是内置链接器。 编译器会对每个文件进行编译,生成 Mach-O(可执行文件);链接器会将项目中的多个 Mach-O 文件合并成一个。

编译的主要过程:

  • 编写好代码后,LLVM 会预先处理你代码,比如把宏嵌入到对应的位置
  • 预处理完成之后,LLVM会对代码进行词法、语法和语义分析,生成 AST。AST 是抽象语法树,结构上比代码更精简, 遍历起来更快,所以使用 AST 能够快速地进行静态检查,同时还能更快地生成 IR (中间代码)。
  • 最后 AST 会生成 IR,IR 是一种更接近于机器语言的语言,区别在于和平台无关,通过 IR 可以生成多份适合 不同平台的机器码。

编译时链接器做了什么

Mach-O 文件里的内容,主要就是代码和数据:代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。不管是代码还是数据,它们的实例都需要有符号将其关联起来。

链接器的作用,就是完成变量、函数符号和其地址绑定这样的任务。这里所说的符号,可以理解为函数名,变量名。链接器在链接多个目标文件的过程中,会创建一个符号表,符号不能重复和找不到。链接器做的是符号和地址绑定,合并项目中的 Mach-O ,链接器主要对代码做了那几件事:

  • 去项目文件里查找目标代码文件里没有定义的变量
  • 扫描项目中的不同文件,将所有符号定义和引用地址收集起来,并放到全局符号表中
  • 计算合并后长度及位置,生成同类型的段进行合并,建立绑定
  • 对项目中不同文件里的变量进行地址重定位

链接器在整理函数的调用关系时,会以main函数为源头,跟随每个引用,并将其标记为 live。跟随完成后,哪些未被标记为 live 的函数,就是无用函数。然后,链接器可以通过打开 Dead code stripping 开关,来开启自动去除无用代码的功能。这个功能,默认是开启的。

动态库链接

链接的共用库分为动态库和静态库:静态库是编译时链接的库,需要链接进 Mach-O 文件里,无法动态加载和更新。动态库是运行时链接的库,使用 dylb 就可以实现动态加载。

Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。

加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击。

系统上的动态链接器会使用共享缓存,共享缓存在 /var/db/dyld/。当加载 Mach-O 文件时,动态链接器会先检查是否有共享缓存。每个进程都会在自己的地址空间映射这些共享缓存,这样做可以起到优化 App 启动速度的作用。

动态链接

Linux/unix 提供了使用 dlopen 和 dlsym 方法动态加载库和调用函数,这套方法在 macOS 和 iOS 上也支持。

  • dlopen:打开一个库获取句柄
  • dlsym:在打开的句柄中查找符号的值
  • dlclose:关闭句柄
  • dlerror: 返回一个描述最后一次调用dlopen、dlsym,或 dlclose 的错误信息的字符串。
#import <dlfcn.h>
typedef int (*printf_func_pointer) (const char * __restrict, ...);
void dynamic_call_function() {
    //动态库路径
    char *dylib_path = "/usr/lib/libSystem.B.dylib";
    
    //打开动态库
    void *handle = dlopen(dylib_path, RTLD_GLOBAL | RTLD_NOW);
    if (NULL == handle) {
        //打开动态库出错
        fprintf(stderr, "%s\n", dlerror());
    }else{
        //获取 printf 地址
        printf_func_pointer printf_func = dlsym(handle, "printf");
        if (printf_func) {
            int num = 100;
            printf_func("Hello exchen.net %d\n", num);
            printf_func("printf function address 0x%lx\n", printf_func);
        }
        //关闭句柄
        dlclose(handle);
    }
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dynamic_call_function();
        Boy *boy = [[Boy alloc] init];
        [boy say];
    }
    return 0;
}

个人思考:因为OC是动态语言,所以编译会把OC中的类的所有方法编译进来,就是默认这些方法都是有用的。故 Dead code stripping 对OC代码无用。

注入动态库实现编译

flutter调试时是如何hotreload UI的。

flutter有两套编译器,JIT,AOT。 debug时用JIT,release时AOT。

debug 时,如果修改了 dart 文件,按下 R 后,Dart 会先去工程里遍历增量 dart 源文件,然后通知 Dart VM 去 load 改写后的 dart 文件,通知 flutter framework 去更新 widgets tree。

Injection for Xcode

只有模拟器可以,真机不可以,这是针对 ARM 64 芯片写的。

使用步骤:

  1. 下载 Injection,Mac App Store不是最新版,在Github上下载
  2. 退出Xcode,打开Injection App,在屏幕右上角,不会出现的Dock栏。通过 Open Project 菜单按钮打开 Xcode 工程所在目录让 Injection 监控其文件变化
  3. 打开Xcode,将 Injection 启动代码加入 AppDelegate,把项目跑在模拟器上

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#if DEBUG
    // for iOS
    [[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];

//    //for tvOS:
//    Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load()
//    //Or for macOS:
//    Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
#endif
    return YES;
}
  1. 在任意继承自OC的类中加入如下需要更新的UI代码,在我们修改了对应文件按下COMMAND + S即可看到UI reload了;
//使用方式一、直接把要变动的代码写这里
- (void)injected{

 	NSLog(@"I've been injected: %@", self);

  self.view.backgroundColor = [UIColor  goldColor];
}
//使用方式二、不用写上面的方法,直接在相应页面修改代码,按下 Command + S 保存后,重新进入这个页面即可看到修改后的效果

在sb或xib页 按下 command + s 保存后,会刷新rootViewController,之前rootViewController不会变,但对应的view是个新的。

对xib或者sb的修改无法及时显现,要重新运行才可以。

  1. 删除方式是,在终端里运行下面这行代码:

rm -rf ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/InjectionPlugin.xcplugin

Injection 会监听源代码

原理

Injection 会监听源代码的变化,如果文件被改动了,Injection Server 就会执行 rebuildClass 重新进行编译、打包成动态库,也就是 .dylib 文件。编译、打包成动态库后使用 writeString 方法通过 Socket 通知运行的 App。

Server 会在后台发送和监听 Socket 消息。Client 也会开启一个后台去发送和监听 Socket 消息。

Client 接收到消息后会运行时进行类的动态替换

链接器:符号是怎么绑定到地址上的

如果你对这篇文章有疑问,欢迎到本站 社区 发帖提问或使用手Q扫描下方二维码加群参与讨论,获取更多帮助。

扫码加入群聊

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

目前还没有任何评论,快来抢沙发吧!

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

2583 文章
29 评论
84935 人气
更多

推荐作者

清风夜微凉

文章 1 评论 0

为你鎻心

文章 2 评论 0

xxhui

文章 0 评论 0

1PKOH46yx8j0x

文章 0 评论 0

Arthur

文章 0 评论 0