Chromium 定制之 FFmpeg 裁剪
一、FFmpeg 的定制
在定制 Chromium 过程中,发现 WebAudio 模块依赖于 FFmpeg,但是只用到了里面的少许接口,因此需要对 FFmepg 做裁剪。
Chromium 源码中提供了对 FFMPEG 整个模块编译的支持,并提供了相关的开关来控制,但是默认该开关是关闭的。
发现 WebAudio 中用到的几个 FFmpeg 中的接口都是 RDFT 相关。接口的实现全都在 avfft.c 这个 C 源文件中。该源文件中还有 FFT、MDCT、DCT 部分的接口实现,这些函数都被预处理指令结合诸如 CONFIG_RDFT、CONFIG_MDCT、CONFIG_DCT 之类的宏定义来控制是否参与编译。
剩下的任务便是从 FFMPEG 中 截取 RDFT 相关接口来做最小化定制。即以 avfft.c 文件出发,编译出一个可用的最小动态库,以满足 WebAudio 模块的需求。
对于 CONFIG_RDFT、CONFIG_MDCT、CONFIG_DCT 等宏的定义,Chromium 针对不同的平台,以及针对 Chrome/Chromium 商标的区分,分别作了预设的配置:
1 2 | sed -n 543p third_party/ffmpeg/ffmpeg.gyp 'platform_config_root': 'chromium/config/<(ffmpeg_branding)/<(os_config)/<(ffmpeg_config)' |
针对ARM 平台,找到了相关头文件的路径:
1 | third_party/ffmpeg/chromium/config/Chromium/linux/arm-neon/config.h |
因此便可通过修改该头文件中相关宏的定义,来控制编译,实现针对 RDFT 模块的最小化定制。
通过不断的尝试,最终完成了 ARM 平台下 FFmpeg 的最小化定制,并且能正常工作。相关技巧见本文第三部分。
二、FFmpeg Wrapper 改造
Chromium 提供了针对 FFmpeg 中相关接口的 wrapper。编译时 WebAudio 模块中关于 FFMPEG 中的 function 调用都链接的是 wrapper function。在 wrapper 中去通过 dlopen/dlsym 的方式去加载关于 FFMPEG 的动态链接库。
继续调查发现,该 wrapper 的源文件是通过 python 脚本根据一个符号列表自动生成的。相关过程粗略介绍如下:
首先,有一个 ffmpegsumo.sigs 的文件,记录着 Chromium 使用到的 FFMPEG 中的全部接口,该文件其实就是一个 C 头文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $ cat third_party/ffmpeg/chromium/ffmpegsumo.sigs | head -n 15 // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. //------------------------------------------------ // Functions from avcodec used in chromium code. //------------------------------------------------ AVCodecContext *avcodec_alloc_context3(const AVCodec *codec); void avcodec_free_context(AVCodecContext **avctx); AVCodec *avcodec_find_decoder(enum AVCodecID id); int av_new_packet(AVPacket *pkt, int size); int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame, int *got_frame_ptr, const AVPacket *avpkt); int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture, int *got_picture_ptr, const AVPacket *avpkt); int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options); |
编译时,通过调用 generate_stubs.py 这个脚本去逐行 parse 该文件中的符号,针对每一个 function 生成对应的 wrapper function。涉及到某些重复的逻辑会用到一些函数模板,一个典型的模板如下:
1 2 3 4 5 6 7 8 | STUB_FUNCTION_DEFINITION = ( """extern %(return_type)s %(name)s(%(params)s) __attribute__((weak)); %(return_type)s %(export)s %(name)s(%(params)s) { if (%(name)s_ptr) { %(return_prefix)s%(name)s_ptr(%(arg_list)s); } }""") |
该模板根据上述符号列表文件中的每一行记录,经 parse 得到 return_type(返回值类型)、name(函数名)、params(参数)、export(可见性控制符)等符号,再去生成 wrapper function。具体输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | extern "C" { // Static pointers that will hold the location of the real function // implementations after the module has been loaded. static RDFTContext * (*av_rdft_init_ptr)(int nbits, enum RDFTransformType trans) = NULL; static void (*av_rdft_calc_ptr)(RDFTContext *s, FFTSample *data) = NULL; static void (*av_rdft_end_ptr)(RDFTContext *s) = NULL; // Stubs that dispatch to the real implementations. extern RDFTContext * av_rdft_init(int nbits, enum RDFTransformType trans) __attribute__((weak)); RDFTContext * av_rdft_init(int nbits, enum RDFTransformType trans) { return av_rdft_init_ptr(nbits, trans); } extern void av_rdft_calc(RDFTContext *s, FFTSample *data) __attribute__((weak)); void av_rdft_calc(RDFTContext *s, FFTSample *data) { av_rdft_calc_ptr(s, data); } extern void av_rdft_end(RDFTContext *s) __attribute__((weak)); void av_rdft_end(RDFTContext *s) { av_rdft_end_ptr(s); } } // extern "C" |
然后这些 wrapper function 以及 dlopen、dlsym 等动作都会被组合并输出到同一个源文件中,最后编译器再去编译生成的源文件。
通过研究该模板发现一个严重的问题:在 dlopen、dlsym 失败的时候,wrapper 模块会将所有的函数指针都置空。该模板生成的 wrapper function 又没有去判断函数指针是否为空,此种情形下一旦被 call 到,就会发生 coredump。
由于 wrapper 模块的源码是由脚本生成的,要去修改相关的逻辑必须修改相应的 python 脚本中的函数模板。
首先,在 wrapper function 中 call 实际的 function 之前,必须要进行空指针的判断。否则返回一个与原返回值类型相同的默认值。根据返回值的不同,又将原有模板拆分为三个不同的模板,分别针对一般函数、void 型函数和指针函数来进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | $ sed -n 84,138p tools/generate_stubs/generate_stubs.py # Template for generating a non-indicator stub function definition with # return value. # Includes a forward declaration marking the symbol as weak. # This template takes the following named parameters. # return_type: The return type. # export: The macro used to alter the stub's visibility. # name: The name of the function. # params: The parameters to the function. # return_prefix: 'return ' if this function is not void. '' otherwise. # arg_list: The arguments used to call the stub function. STUB_FUNCTION_DEFINITION = ( """extern %(return_type)s %(name)s(%(params)s) __attribute__((weak)); %(return_type)s %(export)s %(name)s(%(params)s) { if (%(name)s_ptr) { %(return_prefix)s%(name)s_ptr(%(arg_list)s); } else { %(return_prefix)s%(return_type)s(); } }""") # Template for generating a stub function definition without return value # Includes a forward declaration marking the symbol as weak. # This template takes the following named parameters. # return_type: The return type. # export: The macro used to alter the stub's visibility. # name: The name of the function. # params: The parameters to the function. # arg_list: The arguments used to call the stub function. VOID_STUB_FUNCTION_DEFINITION = ( """extern %(return_type)s %(name)s(%(params)s) __attribute__((weak)); %(return_type)s %(export)s %(name)s(%(params)s) { if (%(name)s_ptr) { %(name)s_ptr(%(arg_list)s); } }""") # Template for generating a indicator stub function definition with return # value. # Includes a forward declaration marking the symbol as weak. # This template takes the following named parameters. # return_type: The return type. # export: The macro used to alter the stub's visibility. # name: The name of the function. # params: The parameters to the function. # return_prefix: 'return ' if this function is not void. '' otherwise. # arg_list: The arguments used to call the stub function. INDICATOR_STUB_FUNCTION_DEFINITION = ( """extern %(return_type)s %(name)s(%(params)s) __attribute__((weak)); %(return_type)s %(export)s %(name)s(%(params)s) { if (%(name)s_ptr) { %(return_prefix)s%(name)s_ptr(%(arg_list)s); } else { %(return_prefix)sNULL; } }""") |
通过这三个模板生成的 wrapper function 举例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $ sed -n 399,420p ./out/Release/obj/third_party/ffmpeg/ffmpeg.gen/ffmpeg_stubs.cc extern int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) __attribute__((weak)); int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) { if (av_rescale_q_ptr) { return av_rescale_q_ptr(a, bq, cq); } else { return int64_t(); } } extern void * av_malloc(size_t size) __attribute__((weak)); void * av_malloc(size_t size) { if (av_malloc_ptr) { return av_malloc_ptr(size); } else { return NULL; } } extern void av_free(void *ptr) __attribute__((weak)); void av_free(void *ptr) { if (av_free_ptr) { av_free_ptr(ptr); } } |
经此调整,修复了 wrapper function 中原有的缺陷,避免了异常情况下的 coredump。
三、技巧总结
3.1、关于 shared libary 的编译
编译 executuble file 与 shared libary,编译器的策略是不同的。编译 executable file,编译器会从 main function 出发,只处理被使用到的所有符号;而编译 shared libary,编译器会处理参与编译的源文件中的所有符号。针对后者,只要没有语法错误与符号冲突,一般编译都会通过。但是在动态加载时(比如 dlopen ),如果目标文件中状态为 UNDEFINED(链接到其他动态库的不算)的并且作用域为 GLOBAL 的符号,就会返回错误。
如果每编译一次,就拿到开发板上测试,效率是相当低的。那么如何在编译之后,不用运行就能确认一个 shared libaray 是可被正常使用的呢?首先,编译通过是必须的。然后,通过查看其中所有对外可见的符号及其状态、作用域等信息,来进行判断。
首先可以通过 readelf 命令加 -s 参数去查找并列出目标文件 libffmpegsumo.so 中的符号。举例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | $ readelf -s out/Release/lib/libffmpegsumo.so | head -n 20 Symbol table '.dynsym' contains 168 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 000021cc 0 SECTION LOCAL DEFAULT 8 2: 00011d18 0 SECTION LOCAL DEFAULT 18 3: 00012018 0 NOTYPE LOCAL DEFAULT 23 __bss_start__ 4: 00092420 0 NOTYPE LOCAL DEFAULT 23 _bss_end__ 5: 00012018 0 NOTYPE LOCAL DEFAULT 22 _edata 6: 00092420 0 NOTYPE LOCAL DEFAULT 23 __bss_end__ 7: 00092420 0 NOTYPE LOCAL DEFAULT 23 _end 8: 00092420 0 NOTYPE LOCAL DEFAULT 23 __end__ 9: 00012018 0 NOTYPE LOCAL DEFAULT 23 __bss_start 10: 00000000 0 FUNC GLOBAL DEFAULT UND pthread_mutex_unlock@GLIBC_2.4 (2) 11: 00006171 20 FUNC GLOBAL DEFAULT 10 ff_fft_calc_vfp 12: 00000000 0 FUNC GLOBAL DEFAULT UND __aeabi_unwind_cpp_pr0@GCC_3.5 (3) 13: 00052440 4096 OBJECT GLOBAL DEFAULT 23 ff_cos_2048 14: 00000000 0 FUNC GLOBAL DEFAULT UND strstr@GLIBC_2.4 (4) 15: 00053440 16384 OBJECT GLOBAL DEFAULT 23 ff_cos_8192 16: 00002ab9 50 FUNC GLOBAL DEFAULT 10 av_fast_realloc |
从以上输出中可以看到目标文件中所有符号的索引号、起始地址、Section 大小、类型、状态以及符号名等信息。在此基础上加上一些过滤条件可以找到那些会导致动态加载失败的符号。下面的命令组合可以有效完成此工作:
1 2 3 4 5 6 | $ readelf -s out/Release/lib/libffmpegsumo.so | grep "UND" | grep -v "@" 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 25: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 77: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 133: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 152: 00000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTabl |
过滤条件“UND”表示只列出其中状态为“UNDEFINED”的符号;-v “@” 表示没有 “@” 符号,即列出没有在其他动态库中定义的符号。
最后,一个命令组合便可确定该动态库是否实际可用:
1 2 | $ readelf -s out/Release/lib/libffmpegsumo.so | grep "UND" | grep -v "@" | grep "GLOBAL"; echo $? 1 |
结论:输出为 1 即表示该动态库可用。
注:分号后一条指令输出前一条指令返的返回值;返回值为 1,即表示最后一条 grep 查找失败,也就是在目标文件中没有找到符合过滤条件(类型为 GLOBAL、状态为 UNDEFINED、并且没有链接到其他动态库中)的符号。
3.2、关于 LICENSE 的检查
编译中遇到的另外一个问题是源文件的 LICENSE 检查。一个直接的方法是查看源文件头部的版权声明。例如 avfft.h 这个头文件中的版权申明是这样的:
1 2 3 4 5 6 7 8 9 | $ cat third_party/ffmpeg/libavcodec/avfft.h | head -n 8 /* * This file is part of FFmpeg. * * FFmpeg is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * |
可以看到该文件使用了 LPGL(GNU Lesser General Public License)许可证。但是针对大量的源文件,这样逐个文件地检查是比较耗时的。无意间发现 Chromium 的源码中有一个 LICENCES 检查的工具 checklicense.py,它既可以检查单个文件,又可以遍历整个目录:
针对单个文件的 LICENSE 检查:
1 2 3 4 5 6 7 8 9 10 | $ ./tools/checklicenses/checklicenses.py -v third_party/ffmpeg/libavcodec/avfft.h Using base directory: /home/chromium/v39/src Checking: /home/chromium/v39/src/third_party/ffmpeg/libavcodec/avfft.h ----------- licensecheck stdout ----------- /home/chromium/v39/src/third_party/ffmpeg/libavcodec/avfft.h: *No copyright* LGPL (v2.1 or later) --------- end licensecheck stdout --------- SUCCESS |
针对目录的 LICENSE 检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | $ ./tools/checklicenses/checklicenses.py -v third_party/ffmpeg/libavcodec/alpha/ Using base directory: /home/chromium/src Checking: /home/chromium/src/third_party/ffmpeg/libavcodec/alpha ----------- licensecheck stdout ----------- /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/idctdsp_alpha.h: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/pixblockdsp_alpha.c: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/mpegvideo_alpha.c: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/regdef.h: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/asm.h: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/blockdsp_alpha.c: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/hpeldsp_alpha.c: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/hpeldsp_alpha.h: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/simple_idct_alpha.c: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/idctdsp_alpha.c: *No copyright* LGPL (v2.1 or later) /home/chromium/src/third_party/ffmpeg/libavcodec/alpha/me_cmp_alpha.c: *No copyright* LGPL (v2.1 or later) --------- end licensecheck stdout --------- SUCCESS |
这个工具结合其他文本处理命令可以迅速完成针对第三方源码库的 LICENSE 分析。