ffmpeg-makefile编译分析 - 弦外之音

/ 0评 / 0

本文以 ffmpeg-n4.4.1 的版本为准,主要分析 ffmpeg 项目中 Makefile 的逻辑。

我的环境是window10 + msys2 ,以这个环境为基础进行讲解。

《ffmpeg-configure编译分析》 之前已经讲解了 configure 的作用,就是根据不同的编译环境,生成了相应的 configure.h 跟 config.mak。例如 如果指定 --toolchain=msvc ,makefile里面的规则 就会使用vs2019的 cl.exe 跟 link.exe 来编译跟链接 C代码。如果没指定 toolchain, 就会默认使用 msys2 里面的 gcc.exe 来编译跟链接 C代码。

msys2 里面执行./configure --toolchain=msvc./configure 生成的 config.mak 差异如下。

从上图可以看到,configure 的时候可以通过 --toolchain 影响编译的时候使用什么样的编译器。



常见错误:

1,Makefile:25: *** missing separator. Stop.

上面的错误基本上是因为 makefile 中要执行的命令,前面不是tab键,而是几个空格,编辑器问题。


在开始分析之前,讲一个 makefile 的调试技巧,推荐阅读《如何调试MAKEFILE变量》

make -f Makefile -f vars.mk HOSTPROGS

这里我对 vars.mk 做了点修改,因为源 vars.mk 没处理特殊字符,直接echo 会报错。ffmpeg makefile 的变量很多特殊字符。我用了 warning 来输出,就不会报错。

vars.mk 文件内容如下:

%:
    $(warning '$*=$($*)')
​
d-%:
#   @printf "%s =888 %s\n" "abc" $($*)
    $(warning '$*=$($*)')
#   @echo '$*=$($*)'
​
#   @echo '  origin = $(origin $*)'
#   @echo '   value = $(value  $*)'
#   @echo '  flavor = $(flavor $*)'

现在从第一行代码开始分析 ffmpeg 里 Makefile 文件的逻辑。不熟悉 makefile 的可以先看一遍《跟我一起写 makefile》

第1~2行代码如下:

MAIN_MAKEFILE=1
include ffbuild/config.mak

定义 MAIN_MAKEFILE ,然后把 config.mak include进来。config.mak 文件里面全是一些变量的定义。

PS:MAIN_MAKEFILE 可以影响改变 config.mak 的 SRC_PATH 变量,估计是处理不同目录的情况。

第4~16行代码如下:

vpath %.c    $(SRC_PATH)
vpath %.cpp  $(SRC_PATH)
vpath %.h    $(SRC_PATH)
vpath %.inc  $(SRC_PATH)
省略

这个其实就是指导make 的时候文件搜索路径,vpath 的用法请阅读 《跟我一起写 Makefile(四)》

第18~19行代码如下:

TESTTOOLS   = audiogen videogen rotozoom tiny_psnr tiny_ssim base64 audiomatch
HOSTPROGS  := $(TESTTOOLS:%=tests/%) doc/print_options

上面的代码只是声明变量,推荐阅读《Makefile 中:= ?= += =的区别》

第37~39行代码如下:

# first so "all" becomes default target
all: all-yes

上面定义了第一个 target (编译目标),makefile的语法是,make的时候如果不指定target ,第一个不以 . 开头的target 就是默认target。推荐阅读《makefile default target》

最后把 all all-yes 定义为 伪目标。伪目标可以防止生成文件。

.PHONY: all all-yes alltools build check config testprogs

这里剧透一下,因为此时 all 这个 target 依赖于 all-yes ,所以make会去找 all-yes 的target,all-yes 的 target 定义在 ffbuild/library.mak 里,如下

all-$(CONFIG_STATIC): $(SUBDIR)$(LIBNAME)  $(SUBDIR)lib$(FULLNAME).pc
all-$(CONFIG_SHARED): $(SUBDIR)$(SLIBNAME) $(SUBDIR)lib$(FULLNAME).pc

第40~41行代码如下:

include $(SRC_PATH)/tools/Makefile
include $(SRC_PATH)/ffbuild/common.mak

引入了两个 Makefile 文件,首先 tools/Makefile 有以下重点:

1,tools\Makefile 里面的有个变量 COMPILE_C 比较复杂,用来编译 tool 目录下的C代码的。

我在shell 里执行了这个命令 ,make tools/target_dec_tiff_fuzzer.o,上图中的 $* 是 % 匹配到的内容,所以$* = tiff

下面是 COMPILE_C 这个变量的具体的值

可以看到 COMPILE_C 实际上执行了3个命令。

2,-include $(wildcard tools/*.d) 前面带一个 - 是防止报错,推荐阅读 《Difference between "include" and "-include" in a makefile

重要知识点:

  1. gcc.exe 或 cl.exe 后面 -D 代表定义一个宏,传递进去C代码里面。例如 -DFFMPEG_DECODER=tiff
  2. makefile 语法,$* 是 % 匹配到的内容,所以$* = tiff

还有一个比较重要的知识点,tools/Makefile 里面使用 COMPILE_C 变量,是 ffbuild/common.mak 里面定义的,但是 tools/Makefile 缺是在 ffbuild/common.mak 前面 include 进去的。

这个要注意一下,makefile的语法 先后顺序 比较模糊的,他是先把所有的 makefile 展开再求值,推荐阅读 《Makefile 中:= ?= += =的区别》,里面的 =:= 充分表明了,makefile 是展开后再求值。


接下来分析 引入 的 ffbuild/common.mak 文件,部分代码如下:

#
# common bits used by all libraries
#
​
DEFAULT_X86ASMD=.dbg
​
ifeq ($(DBG),1)
X86ASMD=$(DEFAULT_X86ASMD)
else
X86ASMD=
endif
​
ifndef SUBDIR
​
ifndef V
Q      = @
ECHO   = printf "$(1)\t%s\n" $(2)
BRIEF  = CC CXX OBJCC HOSTCC HOSTLD AS X86ASM AR LD STRIP CP WINDRES NVCC

makefile 的调试非常麻烦,这里介绍个方法,例如我想知道有没跑进去 ifndef SUBDIR 这个条件,可以使用这个命令 make -f Makefile -f vars.mk d-ECHO 打印出 ECHO 变量。

common.mak 文件都是一些通用的变量定义,不太容易理解的地方有以下:

1,请看下面代码,他这些 规则,command 里面只有一个 $(COMPILE_MMI) ,后面没有接上文件名,看上去不知道要编译什么文件,实际上,要编译的文件名就在 $(COMPILE_MMI) 变量里面,通过上面的 % 匹配到的字符,传递进去给 $(COMPILE_MMI) ,可以把 $(COMPILE_MMI) 打印出来,就能知道具体逻辑。

%_mmi.o: %_mmi.c
   $(COMPILE_MMI)
​
%_msa.o: %_msa.c
   $(COMPILE_MSA)
​
%.o: %.c
   $(COMPILE_C)

2,请看下面代码,target 后面带一个 | ,我也不知道是做什么用的。

$(OBJS):     | $(sort $(dir $(OBJS)))
$(HOBJS):    | $(sort $(dir $(HOBJS)))
$(HOSTOBJS): | $(sort $(dir $(HOSTOBJS)))
$(SLIBOBJS): | $(sort $(dir $(S

第98~107行代码如下:

define DOSUBDIR
$(foreach V,$(SUBDIR_VARS),$(eval $(call RESET,$(V))))
SUBDIR := $(1)/
include $(SRC_PATH)/$(1)/Makefile
-include $(SRC_PATH)/$(1)/$(ARCH)/Makefile
-include $(SRC_PATH)/$(1)/$(INTRINSICS)/Makefile
include $(SRC_PATH)/ffbuild/library.mak
endef
​
$(foreach D,$(FFLIBS),$(eval $(call DOSUBDIR,lib$(D))))

上面这段代码特别重要,有一个命令包DOSUBDIR。在该命令包通过包含libavXXX/Makefile和library.mak等文件,定义了FFmpeg类库(例如libavformat,libavcodec,libavutil等)的依赖关系。

注意看最后一个 ffbuild/library.mak 这个makefile 是最后包含进来的,专门处理之前 include进来的文件。DOSUBDIR 的调用是在一个 foreach 循环里面的,举个例子:

libavdevice\Makefile 文件里面 定义了 DESC = FFmpeg device handling library ,

然后 library.mak 里面就会使用这个 DESC 变量,把他展开,如下,library.mak 第 42 行代码。

$(SUBDIR)lib$(FULLNAME).pc: $(SUBDIR)version.h ffbuild/config.sh | $(SUBDIR)
    $$(M) $$(SRC_PATH)/ffbuild/pkgconfig_generate.sh $(NAME) "$(DESC)"

不同的类库有不同 的 DESC 定义。libavformat\Makefile 的 DESC 定义如下:

DESC = FFmpeg container format library

ffbuild/library.mak 是最重要的一个文件,他就是 make 命令的入口,你可以理解为 main() 函数入口函数,第一个 编译命令就是从这里运行的,具体流程是怎样的?请继续看,之前说过,根目录的 Makefile 在入口定义了 all: all-yes ,所以 all 就是make的默认 target,默认目标。

all 依赖 all-yes,所以就会导致 ffbuild/library.mak 下面这段代码被执行。

all-$(CONFIG_STATIC): $(SUBDIR)$(LIBNAME)  $(SUBDIR)lib$(FULLNAME).pc
all-$(CONFIG_SHARED): $(SUBDIR)$(SLIBNAME) $(SUBDIR)lib$(FULLNAME).pc

上面的 $(CONFIG_STATIC) 展开后是 yes 或者 no,就是 all-yes ,可以看到,他后面有一堆的 prerequisites ,这一堆 prerequisites 就会导致后面继续去找依赖,找 .o 文件,.o 文件没有找到,就会执行 cl.exe 或者gcc.exe 从 C代码编译出来 .o 文件。大概就是这样一个逻辑。可以用make -n 查看具体的编译命令。

所以说 ffbuild/library.mak 是最重要的一个文件


入口找到了,其他的一些编译规则自行查看即可, makefile 分析完毕。

资源下载:

1,带本人注释的 Makefile 下载地址,百度网盘,提取码:f5aj

相关阅读:

  1. 《FFmpeg源代码简单分析:makefile》
  2. 《跟我一起写 makefile》
  3. 《makefile 打印变量的值》

版权所属:知识星球:弦外之音,QQ:2338195090。 由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注