为什么 go 和 rust 这类新兴语言发布程序大都使用静态编译?

2020-12-14 00:46:43 +08:00
 CrazyBoyFeng
尤其是 go,最初的版本里完全不提供动态链接编译。
难道他们没设想过这样的情景:
某天,tls 库被爆出漏洞,go 、rust 发布了一个更新。第二天你需要升级所有依赖 tls 库的 go 、rust 应用,且需要下载的数据量十分巨大(每个应用都包含系统库 /标准库)。

其次就是静态编译对 ram 和 rom 较少的嵌入式设备太不友好。别说全用静态编译了,只要有少数几个应用是自带库编译的,那 ram 和 rom 就得爆炸了。
7957 次点击
所在节点    程序员
57 条回复
hronro
2020-12-14 00:51:34 +08:00
rust 默认不是静态编译吧
optional
2020-12-14 00:51:53 +08:00
go 推崇的是云原生。docker image 本来也不存在更新 so 的情况。
CrazyBoyFeng
2020-12-14 01:02:25 +08:00
@hronro 是近几年 cargo 出来后才慢慢改为默认动态编译。不过一大批古早的 rust 项目都还保留着静态编译的习惯没变。甚至我看到很多 rust 教程都会教人改成静态编译,理由是“没有依赖的问题”。
hronro
2020-12-14 01:04:35 +08:00
@CrazyBoyFeng #3
古早的 Rust 和现在的 Rust 差不多可以当两门不同的语言来看了,最早 Rust 还有 GC 呢
cmostuor
2020-12-14 01:12:52 +08:00
go 本来就不是为嵌入式设备开发的 再说了 rust 和 go 是开源语言 能力强的可以把它魔改一个嵌入式版本的( github 上还真有个 go 语言的嵌入式版)
cmostuor
2020-12-14 01:15:57 +08:00
@cmostuor 动态编译生成 so 库是为了解决内存资源不足的问题而产生的技术现在这年代内存硬盘早就已经白菜价了好几十年为了那点空间而牺牲程序的运行速度 我个人觉得不值得
nightwitch
2020-12-14 01:40:43 +08:00
动态链接的技术发明都快五十年了,语言设计者怎么可能不知道这些缺点。以现在的存储设备和内存的价格,静态链接所增加的那点空间约等于不要钱。而动态链接不仅要增加运行时的开销,而且会符号错误而链接失败浪费程序员的时间,或者链接上了但是实际上 ABI 不兼容导致运行期间 segment fault 。
Jirajine
2020-12-14 01:41:33 +08:00
看标题还以为指静态类型。
你说的是链接,无论静态链接还是动态链接和编译器都没有关系,你想动态完全可以动态链接。
当然这些新语言自带的 build system 会默认以“静态”的方式链接同语言的库( c 库默认还是动态链接),内存硬盘增加、避免依赖问题且不说,主要还是因为 abi 稳定问题。
CrazyBoyFeng
2020-12-14 01:45:55 +08:00
@cmostuor
动态静态不止是性能的问题,还有可维护性的考虑。主楼我就已经阐述了静态编译的其中一种维护灾难。
那个例子还可以再发散一下,如果当前所使用的静态应用没人维护了。那么它所包含的库的漏洞也就没法修复了。

一个程序只做一件事,可能有人会鄙夷这种 unix 哲学。不过在我看来,这是提升一个信息系统整体鲁棒性、安全性的很好的原则。各司其职可能在执行上效率不是最高的,但是当有问题发生时,能更好地处理。

动态静态其实还有一个授权协议的问题,不过这个问题不是十分紧要,foss 软件一般都不会触犯这个问题。
cheng6563
2020-12-14 01:52:10 +08:00
静态链接不等于静态便宜吧
chenqh
2020-12-14 01:56:37 +08:00
因为硬盘不值钱呀, 比内存还值钱
cmostuor
2020-12-14 02:03:02 +08:00
第 9 章 共享库
$Revision: 2.3 $
$Date: 1999/06/15 03:30:36 $
程序库的产生可以追溯到计算技术的最早期,因为程序员很快就意识到通过重用程序
的代码片段可以节省大量的时间和精力。随着如 Fortran and COBOL 等语言编译器的发展,
程序库成为编程的一部分。当程序调用一个标准过程时,如 sqrt(),编译过的语言显式地使
用库,而且它们也隐式地使用用于 I/O 、转换、排序及很多其它复杂得不能用内联代码解释
的函数库。随着语言变得更为复杂,库也相应地变复杂了。当我在 20 年前写一个 Fortran 7
7 编译器时,运行库就已经比编译器本身的工作要多了,而一个 Fortran 77 库远比一个 C++
库要来得简单。
语言库的增加意味着:不但所有的程序包含库代码,而且大部分程序包含许多相同的
库代码。例如,每个 C 程序都要使用系统调用库,几乎所有的 C 程序都使用标准 I/O 库例程,
如 printf,而且很多使用了别的通用库,如 math,networking,及其它通用函数。这就意
味着在一个有一千个编译过的程序的 UNIX 系统中,就有将近一千份 printf 的拷贝。如果所
有那些程序能共享一份它们用到的库例程的拷贝,对磁盘空间的节省是可观的。(在一个没
有共享库的 UNIX 系统上,单 printf 的拷贝就有 5 到 10M 。)更重要的是,运行中的程序如
能共享单个在内存中的库的拷贝,这对主存的节省是相当可观的,不但节省内存,也提高页
交换。
所有共享库基本上以相同的方式工作。在链接时,链接器搜索整个库以找到用于解决
那些未定义的外部符号的模块。但链接器不把模块内容拷贝到输出文件中,而是标记模块来
自的库名,同时在可执行文件中放一个库的列表。当程序被装载时,启动代码找到那些库,
并在程序开始前把它们映射到程序的地址空间,如图 1 。标准操作系统的文件映射机制自动
共享那些以只读或写时拷贝的映射页。负责映射的启动代码可能是在操作系统中,或在可执
行体,或在已经映射到进程地址空间的特定动态链接器中,或是这三者的某种并集。
---------------------------------------------------------------------------------------------
图 9-1:带有共享库的程序
可执行程序,共享库的图例
可执行程序 main,app 库,C 库
不同位置来的文件
箭头展示了从 main 到 app,main 到 C,app 到 C 的引用
---------------------------------------------------------------------------------------------
在本章,我们着眼于静态链接库,也就是库中的程序和数据地址在链接时绑定到可执
行体中。在下一章我们着眼于更复杂的动态链接库。尽管动态链接更灵活更“现代”,但也
比静态链接要慢很多,因为在链接时要做的大量工作在每次启动动态链接的程序时要重新做。
同时,动态链接的程序通常使用额外的“胶合(glue)”代码来调用共享库中的例程。胶合代
码通常包含若干个跳转,这会明显地减慢调用速度。在同时支持静态和动态共享库的系统上,
除非程序需要动态链接的额外扩展性,不然使用静态链接库能使它们更快更小巧。
绑定时间
共享库提出的绑定时间问题,是常规链接的程序不会遇到的。一个用到了共享库的程
序在运行时依赖于这些库的有效性。当所需的库不存在时,就会发生错误。在这情况下,除
了打印出一个晦涩的错误信息并退出外,不会有更多的事情要做。
当库已经存在,但是自从程序链接以来库已经改变了时,一个更有趣的问题就会发生。
在一个常规链接的程序中,在链接时符号就被绑定到地址上而库代码就已经绑定到可执行体
中了,所以程序所链接的库是那个忽略了随后变更的库。对于静态共享库,符号在链接时被
绑定到地址上,而库代码要直到运行时才被绑定到可执行体上。(对于动态共享库而言,它
们都推迟到运行时。)
一个静态链接共享库不能改变太多,以防破坏它所绑定到的程序。因为例程的地址和
库中的数据都已经绑定到程序中了,任何对这些地址的改变都将导致灾难。
如果不改变程序所依赖的静态库中的任何地址,那么有时一个共享库就可以在不影响
程序对它调用的前提下进行升级。这就是通常用于小 bug 修复的"小更新版"。更大的改变不
可避免地要改变程序地址,这就意味着一个系统要么需要多个版本的库,要么迫使程序员在
每次改变库时都重新链接它们所有的程序。实际中,永远不变的解决办法就是多版本,因为
磁盘空间便宜,而要找到每个会用到共享库可执行体几乎是不可能的。
实际的共享库
本章余下的部分将关注于 UNIX System V Release 3.2 (COFF 格式),较早的 Linux 系
统(a.out 格式),和 4.4BSD 的派生系统(a.out 和 ELF 格式)这三者提供的静态共享库。这三
者以几近相同的方式工作,但有些不同点具有启发意义。SVR3.2 的实现要求改变链接器以支
持共享库搜索,并需要操作系统的强力支持以满足例程在运行时的启动需求。Linux 的实现
需要对链接器进行一点小的调整并增加一个系统调用以辅助库映射。BSD/OS 的实现不对链接
器或操作系统作任何改变,它使用一个脚本为链接器提供必要的参数和一个修改过的标准 C
库启动例程以映射到库中。
地址空间管理
共享库中最困难的就是地址空间管理。每一个共享库在使用它的程序里都占用一段固
定的地址空间。不同的库,如果能够被使用在同一个程序中,它们还必须使用互不重叠的地
址空间。虽然机械的检查库的地址空间是否重叠是可能的,但是给不同的库赋予相应的地址
空间仍然是一种“魔法”。一方面,你还想在它们之间留一些余地,这样当其中某个新版本
的库增长了一些时,它不会延伸到下一个库的空间而发生冲突。另一方面,你还想将你最常
用的库尽可能紧密的放在一起以节省需要的页表数量(要知道在 x86 上,进程地址空间的每
一个 4MB 的块都有一个对应的二级表)。
每个系统的共享库地址空间都必然有一个主表,库从离应用程序很远的地址空间开始。
Linux 从十六进制的 60000000 开始,BSD/OS 从 A0000000 开始。商业厂家将会为厂家提供的
库、用户和第三方库进一步细分地址空间,比如对 BSD/OS,用户和第三方库开始于地址 A08
00000 。
通常库的代码和数据地址都会被明确的定义,其中数据区域从代码区域结束地址后的
一个或两个页对齐的地方开始。由于一般都不会更新数据区域的布局,而只是增加或者更改
代码区域,所以这样就使小更新版本成为可能。
每一个共享库都会输出符号,包括代码和数据,而且如果这个库依赖于别的库,那么
通常也会引入符号。虽然以某种偶然的顺序将例程链接为一个共享库也能使用,但是真正的
库使用一些分配地址的原则而使得链接更容易,或者至少使在更新库的时候不必修改输出符
号的地址成为可能。对于代码地址,库中有一个可以跳转到所有例程的跳转指令表,并将这
些跳转的地址作为相应例程的地址输出,而不是输出这些例程的实际地址。所有跳转指令的
大小都是相同的,所以跳转表的地址很容易计算,并且只要表中不在库更新时加入或删除表
项,那么这些地址将不会随版本而改变。每一个例程多出一条跳转指令不会明显的降低速度,
由于实际的例程地址是不可见的,所以即使新版本与旧版本的例程大小和地址都不一样,库
的新旧版本仍然是可兼容的。
对于输出数据,情况就要复杂一些,因为没有一种像对代码地址那样的简单方法来增
加一个间接层。实际中的输出数据一般是很少变动的、尺寸已知的表,例如 C 标准 I/O 库中
的 FILE 结构,或者像 errno 那样的单字数值(最近一次系统调用返回的错误代码),或者
是 tzname (指向当前时区名称的两个字符串的指针)。建立共享库的程序员可以收集到这些
输出数据并放置在数据段的开头,使它们位于每个例程中所使用的匿名数据的前面,这样使
得这些输出地址在库更新时不太可能会有变化。
cmostuor
2020-12-14 02:11:08 +08:00
第 10 章 动态链接和加载
$Revision: 2.3 $
$Date: 1999/06/15 03:30:36 $
动态链接将很多链接过程推迟到了程序启动的时候。它提供了一系列其它方法无法获
得的优点:
动态链接的共享库要比静态链接的共享库更容易创建。
动态链接的共享库要比静态链接的共享库更容易升级。
动态链接的共享库的语义更接近于那些非共享库。
动态链接允许程序在运行时加载和卸载例程,这是其它途径所难以提供的功能。
当然这也有少许不利。由于每次程序启动的时候都要进行大量的链接过程,动态链接
的运行时性能要比静态链接的低不少,这是付出的代价。程序中所使用的每一个动态链接的
符号都必须在符号表中进行查找和解析( Windows 的 DLL 某种程度上有所改善,下面将会讲
到)。由于动态链接库还要包括符号表,所以它比静态库要大。
在调用的兼容性问题之上,一个顽固的麻烦根源是库语义的变化。和非共享或静态共享库而
言,变更动态链接库要容易很多。所以很容易就可以改变已存在程序正在使用的动态链接库。
这意味着即使程序没有任何改变,程序的行为也会改变。在微软 Windows 系统下这是一个常
见的问题所在,程序会使用大量的共享库,而这些库有不同的版本,库之间的版本控制非常
的复杂。多数程序在出货时都带有它们所需库的副本,而安装程序经常会不假思索的将安装
包中的旧版本共享库覆盖已存在的新版本库,这就破坏了那些依赖新版本库特性的程序。考
虑周全的安装程序会在使用旧版本库覆盖新版本库的时候弹出告警框提示,但这样的话,依
赖新版本库特性的那些应用程序又会发生旧版本库替换新版本库时发生的类似问题。
nuk
2020-12-14 02:21:34 +08:00
某一天,tls 爆出漏洞,楼主给自己的机器升级 ssl 的时候顺便更新了 libcrypt
然后楼主再也 ssh 不上这台机器了

这是发生在我公司的真实案例,还好客户就在本市,修复很快,不用赔钱
shyling
2020-12-14 02:43:25 +08:00
因为过去你说几 m 的库挺大,现在这个东西都算很小很小了
CrazyBoyFeng
2020-12-14 02:50:12 +08:00
@nuk 所以,你们认为现代系统应该抛弃 so 、dll 库,崇尚发行更多的静态软件?
(不过刚才去查了一下还真有这种东西,stali 和 suckless,不知道实际体验怎么样)
nuk
2020-12-14 03:09:49 +08:00
@CrazyBoyFeng 没有啊。。现代系统随便他怎么发展,只是静态链接对应用方便一点
不瞒你说,我这公司内有一些软件还依赖 openssl1.0,但是没人愿意花时间打 patch,后来就全部静态链接了
你说的也都有道理,可是禁不住大家想偷懒
动态库还有一个比较麻烦的问题,就是非 root 是没办法 LD_PRELOAD 啥的,而且 LD_PRELOAD 在 fork 的时候又容易有问题,搞个静态编译不就啥事都没了?
现在动态库的 ABI 破坏性更新太多了,又没有类似 windows 上 manifest 的技术,如果有的选,C 程序我是不想用静态编译的,一些库改成静态编译要花不少功夫。
akira
2020-12-14 03:50:14 +08:00
构建的时候有各种版本依赖是可以接受的。
运行环境带各种版本依赖才是灾难
lovestudykid
2020-12-14 05:17:25 +08:00
静态编译的话用户方便吧,下载用的流量和磁盘空间,比起向用户解释清楚各种依赖的麻烦不值一提
webshe11
2020-12-14 05:41:45 +08:00
一次编译,各种发行版 /docker image 运行
搭环境运行省事了,但是确实,如果库也要升级就麻烦了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/735084

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX