golang语言编译的二进制可执行文件为什么比 C 语言大 最近一位朋友问我“为什么同样的hello world 入门程序”为什么golang编译出来的二进制文件,比 C 大,而且大很多。我做了个测试,来分析这个问题。C 语言的hello world程序: golang 语言的hello world程序: 编译,查看生成文件大小 正如这位朋友所说c.out是12K,而 go_fmt.out是1.5M,差距奇大无比....为什么呢? 这两个二进制可执行文件文件里,都包含了什么? 众所周知,linux 上的二进制可执行文件是 ELF Executable and Linkable Format 可执行和可链接格式 如上图,ELF 文件分为如下: ELF文件的组成:ELF header 程序头:描述段信息 Section头:链接与重定位需要的数据 程序头与Section头需要的数据.text .data 在 Linux 上, 查看elf格式构成可以使用readelf ELF Header:头的信息 ELF 头的长度都是一样的,不会带来总体体积的变化。区别是个别字节的值不一样,比如Entry point address 程序入口点的值不一样等。 接下来是 程序头:,也就是 section部分(在linker连接器的角度是section部分或者装载器角度的segment) 可以看到c.out里引用了一个动态链接库libc.so.6,再看下go_fmt.out的情况 c.out的执行,依赖了libc.so.6, libc.so.6肯定需要ld.so的,看下依赖情况, 也就是说,C的程序默认使用了libc.so动态链接库,go 的程序,默认进行了静态编译,不依赖任何动态链接库。所以体积变大了。 那么,只是这一个原因吗? 我在 golang 的官方文档里找到如下的解释: Why is my trivial program such a large binary? The linker in the gc tool chain creates statically-linked binaries by default. All Go binaries therefore include the Go run-time, along with the run-time type information necessary to support dynamic type checks, reflection, and even panic-time stack traces. A simple C "hello, world" program compiled and linked statically using gcc on Linux is around 750 kB, including an implementation of printf. An equivalent Go program using fmt.Printf is around 1.5 MB, but that includes more powerful run-time support and type information. 将c的程序也使用静态编译试试。。。 可以看到,使用静态编译生成的二进制文件c_static.out为888K,仍然比 GO 写的小了一半,这到底是为什么呢?到底是哪里多了? 在ELF 可执行文件里,就需要以程序编译链接的角度来分析了,对于一个 ELF 文件的分析,上面部分分析过 ELF header部分,以及 dynamic section的情况了。再以看一下剩余的section信息。 ELF中的section主要提供给Linker使用, 而segment提供给Loader用,Linker需要关心.text, .rel.text, .data, .rodata等等,关键是Linker需要做relocation。而Loader只需要知道这个段的Read、Write、Execute的属性。 再去看go_fmt.out里都包含了什么,为了方便校对,写了一个程序来对比 对比一下两个文件的section段信息 发现go_fmt.out多了好多.debug_*开头的 section,这是用于 debug 的段信息。再次编译,去除这些信息,同时也把 C 静态编译的二进制也去除符号表和重定位信息。 如上结果,go_fmt_sw.out为1012K,c_static_gs.out为820K,还大了近200KB。到底是哪里大的呢? 刚刚的两个elf 文件的section对比中,还有一个比较特殊的go_fmt.out中 有一个名字叫.gopclntab的段,类型是SHT_PROGBITS程序段,大小为 282414字节,也就是275K,在c_static.out里并没有这个段的,也没有.gosymtab这个段。二者不一样,section段名字有规范标准吗? 其实,对于linker链接器来说,会关心段(section)的名字,但对loader加载器来说,并不关心名字,只关心这个段(segment)的权限,是否可执行,所在的偏移地址,用于函数的执行。 那.gopclntab段包含了什么内容呢?我写了一个程序分析了这个段的内容,程序代码如下: 编译后执行 如上可以看到,有很多函数是以fmt.(*pp)、strconv.*、sync.*、reflect.*、unicode.*等开头的,后面对应的函数名,也与 golang 的包里对应的包中函数名一致。。。用 IDA来确认一遍 ,果然在 .gopclntab 段里有很多 reflect.*开头的函数。 这就很奇怪了,golang 编译时,默认把 runtime 包编译进来就好了,应该不会把strconv\sync\reflect\unicode等包包含进来啊。程序中,只写了一句fmt.Println(),莫非是fmt包import了其他几个包导致的?回去搜了下代码,果然... 嗯,应该是这里问题,改用 go 的内置函数print试试。 编译后,对比大小 看如上结果,go_print_sw.out 变成了 624K , c_static_gs.out为820K,不光没比C的静态编译的大,还比它小呢。。。 不过呢,这也不能说明什么问题,只是因为其包含的函数内容不一样。 好了,至此已经知道为什么 golang 编译的文件比 C 的大了,因为 go 语言是静态编译的,而 C 的编译(比如 gcc编译器)都是动态链接库形式编译的。所以,导致了 go 编译的文件稍微大的问题。其次,跟其他语言比较字符串输出的话,用print内置函数就好了,就不要使用fmt包下的函数来比较了,因为 fmt 包引入了好多其他的包。。。这也增加编译后的二进制文件的体积。 其实呢,golang 的编译(不涉及 cgo 编译的前提下)默认使用了静态编译,不依赖任何动态链接库,这样可以任意部署到各种运行环境,不用担心依赖库的版本问题。只是体积大一点而已,存储时占用了一点磁盘,运行时,多占用了一点内存。早期动态链接库的产生,是因为早期的系统的内存资源十分宝贵,由于内存紧张的问题在早期的系统中显得更加突出,因此人们首先想到的是要解决内存使用效率不高这一问题,于是便提出了动态装入的思想。也就产生了动态链接库。在现在的计算机里,操作系统的硬盘内存更大了,尤其是服务器,32G、64G 的内存都是最基本的。可以不用为了节省几百 KB 或者1M,几 M 的内存而大大费周折了。而 golang 就采用这种做法,可以避免各种 so 动态链接库依赖的问题,这点是非常值得称赞的。 另外,.gopclntab 跟 .gosymtab段的讨论,以及其他建议 https://groups.google.com/forum/#!topic/golang-dev/EJhwBrITcpA https://groups.google.com/forum/#!msg/golang-dev/EJhwBrITcpA/EMHeWpriGZUJ 如果想要ELF二进制程序变小的方法,还可以使用 linux 的strip,以及 UPX 来压缩 ELF 文件。 推荐一本书《程序员的自我修养-链接、装载与库》链接 参考: 《程序的链接和装入及Linux下动态链接的实现》 《Reversing GO binaries like a pro》 2017-09-21 +10 14 0
使用APC来保护PHP代码 问题就那么发生了: PHP的项目在生产服务器运行时,基本都是明文的文件,如果要保护代码的话,多数使用商业的加密软件,比如Zend Guard或者其他软件。zend也就是php程序语言Zend Engine语法分析引擎的公司出品。该产品会对php代码进行加密转换,费用大约是$600每年(大约4000人民币),按照公司授权。zend guard收费倒不是主要问题,如果单单购买加密功能的话,项目性能会奇差,相信很多朋友都遇到了,尤其是webgame中,刚开服时,几乎都是cpu 100%,load 奇高。使用时,需要服务端在装一个解密的程序,每个脚本文件,在每次被访问时,都需要解码、验证授权。这种繁琐的步骤,无疑浪费了时间、系统资源,也是我们一个项目当初在“友好邻邦”服务器上出现超高占用CPU的原因。(OS load占用高达100倍) 这些都是些什么玩意: 这产品是加密,如果想要程序执行流程优化的话,Zend又很“周到”的为我们提供另外一款产品 Zend Server,每年只需要最低支付$1695美元(大约11000人民币)(其他黄金版、白金版需要上万美元)。。。这样一来,我们的产品被绑架在zend上,如果我们需要定制功能的话,那是不可想象的。 如果是zend产品有bug,那我们只能干等着,等他们解决。除了zend这款,还有另外一款代码加密产品:ioncube,费用倒是不高,大约400美元每年。但用户量少,产品是否稳定不清楚,不开源,社区支持不是很好。 找寻: 有没有一款可以保护代码,社区支持好,用户量大,反馈处理及时,费用低的产品吗? 幸运的是,有这么一款开源产品APC:http://pecl.php.net/package/APC Alternative PHP Cache (APC)是一款php代码加速产品,该产品作为php程序的一个拓展,是一个开放自由的PHP opcode 缓存。它的目标是提供一个自由、 开放,和健全的框架用于缓存和优化PHP的中间代码。 早在3-4个月之前,鸟哥博客上一篇文章《关于PHP的编译和执行分离》中提到APC来作为PHP代码保护的方案。从文中可以看出,鸟哥的想法是每个php文件,导出一个opcode 的bin文件,加载时,也是挨个加载,这样也实现了代码保护,但一个项目几百个php文件的话,也得相应存在几百个bin文件,量比较大,操作比较复杂,管理不方便,不好做版本验证(以后会提到)。末学比较倾向于单个bin文件的导出,单个opcode bin文件的加载。而且,单个bin文件的加载,可以避免项目中出现部分文件跟整体版本不一致的情况发生,运维同事再也不用担心个别文件跟整个项目版本不一致的情况了。 原来是你: APC是替换了php zend引擎的zend_compile_file函数,来决定是否重新对php脚本文件的读取,扫描、解释、编译等步骤。启用apc之后,将跳过读取、扫描、解释、编译这几步,节省内存、CPU开销,直接执行OPCODE。Apc还提供对php源码的opcode的缓存,导出到一个二进制文件中,还提供加载这个二进制文件。起初,我们开始使用这个功能时,遇到该拓展的多个BUG,都已经提交到php官方,分别是: BUG #62757 、BUG #62765 、BUG #62825 ,并积极配合php内核组开发成员重现BUG,抓获coredump,提取bug demo代码,在PHP官方成员-APC拓展开发组长Xinchen Hui(以下称鸟哥 laruence)的帮助下,很快的修复了这些BUG 。 如此繁琐: 这样仍存在一个繁琐的步骤,即php-fpm每次启动时,没有自动加载bin文件,需要管理员去手动执行或者访问脚本,(该脚本内使用apc_bin_load/apc_bin_loadfile函数去加载bin文件)让php-fpm加载bin文件,对于单大区多台前端,以及单服务器运行多个php-fpm主进程来说,如何判断那个php-fpm主进程是否成功加载,这是个很麻烦的问题。对于程序执行opcode清除之后,又需要再次加载,这样是个非常复杂繁琐的过程,也容易出问题。 自力更生: 可否在fpm启动时,自动加载bin的功能呢?末学阅读了APC的源码,照葫芦画瓢的添加了自动加载的opcode bin文件的功能。 我们是在 APC SVN http://svn.php.net/viewvc/pecl/apc/trunk/ 的 r327454版本基础上,在apc_module_init函数中,模块初始化时,增加了opcode bin文件的自动加载功能,patch代码见:https://github.com/cfc4n/cnxct/blob/master/apc_r327454_add_preload_binfile.patch 。有兴趣的朋友可以git使用下。当然,末学水平有限,难免存在BUG,还请见谅。 后来,末学将此patch提交到PHP官方,请他们审核一下,是否可以收入apc官方中,免得我们以后都一直作为patch来使用。或者协助完善,或者给出更好的解决办法。 很伤心,未被采纳,原因是这是小众的需求。但末学表示不解,那apc_preload_path这个自动加载user cache的功能不也是小众需求吗?或许,这是他们委婉的拒绝方式。 (同时,还发现了APC自带的一个未公布的功能,自动加载user cache的功能,配置项为apc.preload_path,配置的值是目录地址,目录下可有多个文件,这些文件也应该是apc_bin_dump/apc_bin_dumpfile函数导出的user cache。看来,官方也有这么个打算。但为何没有实现 opcode bin文件 的自动加载呢?) 如何使用: opcode bin文件导出与导入: 导出的php脚本,末学提供一份,需要的朋友,可以参照修改一下即可: https://github.com/cfc4n/cnxct/blob/master/apc_dump.php 导出成功后,会在PROJECTROOT目录下面生成一个dumproot目录,dumproot目录中的所有文件,就是你需要上传到生产服务器上的文件。bin目录中,会生成两个文件,一个是opcode的bin文件,一个是生成之前的php文件的md5 hash值。其他目录就是项目被保护的目录,目录中文件都是空的php文件。将dumproot目录上传至生产服务器,保持项目路径正确,重启fpm即可。可通过apc拓展源码包内的apc.php查看被缓存文件数。如图: 版本更新、回滚: 遇到bug需要修复时,只需要在opcode bin文件导出服务器上,重新导出一个bin文件,将此bin文件上传到生产服务器,替换到之前的bin文件即可。偶尔会遇到项目因种种原因,需要对项目进行回滚,如果使用我们的方案之后,会发现所有php文件都已经是空的了。而我们的回滚更方便,只需要将之前需要回滚的版本的bin文件,替换到当前的文件即可。或者更改php.ini中apc.preload_binfile的路径。 版本检测 当怀疑个别文件不是某个版本的程序是,怎么办呢?apc提供了另外一个参数 apc.file_md5来存储被cache文件的md5 hash。但当使用已到处的opcode bin文件时,apc却没有重新抓去md5 hash,也没有重新存入apc中,导致我们看到的md5值是个错误的hash值。为此,末学斗胆做了修改,并提供了patch,且提交该BUG #63491到pecl apc官方,斗胆请官方采纳。当打上该patch之后,您可以在apc.php 看到这些信息。并且,跟导出bin文件时,生成的md5 hash文件中校对以确认哦。(2012/12/17 更新,官方已修复) 批量检测也是可以的,参加末学另一个脚本:https://github.com/cfc4n/cnxct/blob/master/check.php APC其他已知bug: 一、php5.4.x中,class中的array()静态成员属性在跨服务器使用时,产生core,已经提交到 BUG #63636(此bug由recoye发现) 临时解决方案如下: 使用php5.4.x时,不要在class中定义 $_user = array(),直接定义为 $_user; 不要使用php5.4.x 删除或注释apc_compile.c的1992、2003行(apc 3.1.13),即 zval_ptr_dtor(&src->default_static_members_table[i]); 字样的代码。 等待PHP官方解决,或微博@鸟哥,催鸟哥解决 二、web访问apc_dump.php 去生成的bin文件,在cli模式下使用时,会在RSHUTDOWN时,即请求结束时,回收系统资源时,会产生core。(该结果的php版本为php5.3.x,初步确认为上一个bug有密切关系,暂未提交至bugs.php.net) 这两个bug以末学的理解来看,与末学的patch无关,为apc的bug。 注意事项: 导出bin文件的服务器上,不要开启 apc.preload_binfile ,避免bin文件存在时,使用bin文件内的代码,而不使用php文件内的代码,导致导出的代码不是你想要的。 php5.4.x class中array 静态成员以及属性 操作系统32bit、64bit的导出的bin文件不能相互乱用。(乱用将产生core) 导出服务器的web路径跟目标生产服务器的web路径一致 生产服务器使用bin文件之后,必须存在同名的php文件,内容可以为空。 apc.ttl、apc.gc_ttl参数保持为0 apc.serializer 如果改用其他序列化函数发生问题,那么请保持为空,或者为 apc.serializer=php apc.num_files_hint的值务必大于等于cache files 的数量,可以先设置大点,再通过生产服务器上观察一段时间来确认 啰嗦一下: 关于32bit\64bit服务器上导出opcode bin文件的乱用问题,将产生如下core 因为long之类类型,在不同bit操作系统上的长度不一致导致内存越界的BUG。 末学不知道这算不算bug,或者您别混用,或者APC程序上也可以解决... 惊喜: 不光可以代码保护,性能提升,方便运维,还可以防黑客入侵哦。(项目目录不允许创建新文件,那么即使黑客改了php脚本,php仍会去apc里读,也不会去读取它。哪怕黑客执行了 apc_cache_clean清除opcode cache,我们的补丁仍会自动加载,那么黑客的行为还是阻拦了。) 备注: 此patch已经在我们的“友好邻邦”服务器上稳定运行3-4个月左右,无异常案例,各位可放心使用。 Apc不算一个php encoder,算是个cache,一个opcode cache,起到中间码缓存的作用,故本文中用“代码保护”,而不是“代码加密”。 但实质上实现了我们的目的,起到保护源代码的作用,那怕可以逆向,也是有一定难度的。 PS:感谢Ivon的openstack,感谢终极修炼师的协助测试。 PPS: 如果你能保证php文件不会被SAPI形式访问到的话,只会被include/require等访问到的话,甚至连同名空文件都不用放。比如单入口框架的项目,只要放个空的入口文件即可。 (感谢recoye纠正) PPPS: 更新日期:2014/10/17 近来,好多朋友问我要完整版,鉴于apc已经被官方删了,那个SVN地址无法正常checkout,故我在硬盘里找了个压缩包,应该是可用的版本,如果有其他异常,请留言,谢谢。 适用于PHP5.3的完整拓展:点击这里下载包含patch的完整版apc 2012-11-29 +39 43 0