动态链接和静态链接及交叉编译的思考


背景

跟同事聊天的时候,偶然聊到了要写一些C++的库供多个平台使用,也就是Android,IOS,服务端等。例如Android应用可以编译成.so使用,服务端也可以编译成.so使用,避免了一套逻辑多个平台都要写一遍的问题。

那么问题来了:

  • 一般软件交付是动态链接多还是静态链接多呢?
  • 静态链接的可执行程序为什么不能在多个系统间使用?g
  • go交叉编译的时候,为什么有的程序是动态链接,有的是静态链接呢?

小朋友,你的脑袋里是否有很多问号。如果有的话,很好,那我们就一起学习下吧。

软件交付

静态和动态可执行程序的区别

静态可执行程序
  1. 文件较大,因为包含所有依赖的的库
  2. 相同操作系统之间可以平移,不依赖系统版本
  3. 启动速度快,不需要动态链接步骤
动态可执行程序
  1. 比静态可执行程序文件小,依赖动态库,例如系统的一些库可以共享
  2. 相同操作系统迁移有风险,动态库版本可能不一致。

交付软件要解决的问题,一个是用户会在不同平台安装,一个是用户的系统版本和配置有高有低。如果站在兼容性的角度,似乎交付不需要额外依赖的静态可执行程序好一些。但实际上并非如此。

常见软件交付的选择

一般市面上常用的方式就是的:动态可执行程序 + 动态库的方式。
软件厂商在交付的时候,会把程序依赖的动态库例如.so和.dll一起下来下来,并且设置程序的动态链接路径(linux是LD_LIBRARY_PATH)。在程序执行的时候,优先去设置的动态链接地址寻找动态库,找不到的话才会去系统对应的地址下寻找动态库。

动态可执行程序的好处
  1. 节省磁盘空间和内存:对于动态可执行程序,多个程序可以共享同一个动态库的只读部分(如代码段),这样就可以减少磁盘和内存的占用。
  2. 降低更新成本:当动态库需要更新时,只需要替换动态库文件,而不需要重新编译和链接所有依赖这个库的程序。用户也只需要更新动态库,而不需要重新下载整个程序。
  3. 方便扩展和插件系统:动态库可以在程序运行时进行加载和卸载,这为程序的动态扩展提供了可能。例如,许多软件的插件系统就是基于这个特性实现的。

举个栗子

Mac下的微信WeChat

查看文件格式

file WeChat
WeChat: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]
WeChat (for architecture x86_64):	Mach-O 64-bit executable x86_64
WeChat (for architecture arm64):	Mach-O 64-bit executable arm64

查看动态链接路径

otool -L WeChat
WeChat (architecture x86_64):
/System/Library/Frameworks/xxx : 系统的动态库
@executable_path/../Frameworks/libWxVcodec2Dyn.dylib : 下载微信自带的动态库

这里的executable_path就是可执行程序的路径,根据可执行程序的相对路径去找到下载自带的动态库。

Linux下的vscode

查看文件格式

➜  visual-studio-code file code
code: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), 
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=21e24eb532ea9576c16d553b055f7ef7e945e6dd, stripped

查看动态链接路径

 ls
bin                     chrome_crashpad_handler  icudtl.dat    libGLESv2.so          LICENSES.chromium.html  resources.pak            vk_swiftshader_icd.json
chrome_100_percent.pak  chrome-sandbox           libEGL.so     libvk_swiftshader.so  locales                 snapshot_blob.bin
chrome_200_percent.pak  code                     libffmpeg.so  libvulkan.so.1        resources               v8_context_snapshot.bin

好家伙,依赖的.so直接都在根目录了。

静态可执行程序可以跨系统吗

答案是不行的,静态可执行程序虽然可以把依赖的库都编译进去,但是还有两个问题需要解决。

可执行程序格式

  1. Linux 系统下编译的可执行文件通常是 ELF(Executable and Linkable Format)格式
  2. Windows 系统能识别并执行的是 PE(Portable Executable)格式的文件
  3. Mac系统能识别并执行的是Mach-O (Mach object file format) 格式的文件

操作系统通过解析特定格式的头部信息来确定如何加载和执行这个文件。因此,如果没有经过专门的转换(即交叉编译),操作系统无法解析其他操作系统的可执行文件。

系统ABI版本

全称是"Application Binary Interface",即应用程序二进制接口。它是一个接口标准,定义了应用程序和操作系统之间或者不同应用程序模块间所必须遵守的一个约定。
这关乎到应用程序的二进制兼容性问题。你可以把它看作是一种约定,即:

  • 如何在运行时找到并执行程序的入口函数
  • 系统如何调用在程序二进制文件中定义的函数,以及
  • 如何将参数从调用函数传递给被调用函数,以及从被调用函数返回结果给调用函数。

可执行程序中执行时仍然需要通过系统调用与操作系统进行交互(比如文件读写、网络通讯等),这些操作都依赖ABI接口协议,接口协议不同,执行也会出现问题。

所以本质上,静态编译解决的是相同操作系统间的依赖问题。跨系统的话还是需要交叉编译或者各个系统编译各个系统的版本的。

执行可执行程序的步骤

例如windows系统:

  1. 双击打开,此时操作系统会创建新的进程
  2. 读取磁盘上的文件,转交给操作系统
  3. 装载程序,此时装载器会读取文件,例如PE可执行程序,加载到内存
  4. 加载动态链接,如果可执行程序是动态链接,那么会去找对应的动态链接库
  5. 执行程序,也就是找到程序的入口函数,例如main()
  6. 操作权交给程序进程,执行程序

根据以上操作,可以看到如果是跨操作系统的话,第3步读取可执行程序就会报错。如果缺少动态库的话第4步会报错。这也解释为啥有时候打开程序会提示你缺少xxx.dll,需要自己去下载。

GO交叉编译的疑问

静态编译的静态库哪来的

go默认采用静态链接。默认情况下,Go的runtime环境变量CGO_ENABLED=1,即默认开始cgo,允许你在Go代码中调用C代码,go compiler会重新编译依赖的包的静态版本,包括net、mime/multipart、crypto/tls等,并将编译后的.a(以包为单位)放入临时编译器工作目录($WORK)下,然后再静态连接这些版本。
也就是说,go会把依赖的包编译成静态版本,然后进行链接。
如果go程序依赖了系统的C运行时库,静态编译的情况下,会把运行时库静态链接到可执行程序,会导致可执行程序变得很大。尽量不要使用CGO,构造CGO的交叉编译环境还是挺麻烦的。

动态编译的.so哪来的

跟上面对应的,go程序依赖系统的C运行时库的时候,默认就会生成动态链接的可执行程序。此时可以通过ldd查看:

linux-vdso.so.1 (0x00007ffc3d3c5000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff5bd483000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff5bd876000)

要使用什么动态库以及动态库的版本是通过编译器来写入到可执行程序的。
我们在ldd的时候,会去系统的动态链接库路径中去寻找动态库,例如LD_LIBRARY_PATH, ld.so.cache 文件和一些预设的库路径(比如 /lib 和 /usr/lib)等。
所以说,在windows上交叉编译go程序,并不会直接生成.so给可执行程序使用,而是设置可执行程序依赖的.so库,然后到目标系统(Linux)上自己找.so的路径。如果找不到.so或者版本不对,那就报错。

这篇文章让博主想起很久前的一道面试题:为什么windows的程序在mac上跑不起来?以前只知道是装载器识别不了,现在总算明白了,文件格式不对,ABI接口协议也对不上。

end

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁柱同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值