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

发布于 2021-03-05 13:11:24 字数 7046 浏览 1375 评论 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 接收到消息后会运行时进行类的动态替换

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

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

0 文章
0 评论
84960 人气
更多

推荐作者

xu362930323

文章 0 评论 0

缱倦旧时光

文章 0 评论 0

qq_eXruk9

文章 0 评论 0

遂心如意

文章 0 评论 0

guojiayue1

文章 0 评论 0

愿与i

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文