第一章 HelloWorld背后没有那么简单

01_交叉编译Hello程序

时空小警察来喽,给韦老师的课一点AI震撼(bushi。

使用GCC对Hello.c进行编译。

gcc -o Hello Hello.c

在终端里面修改传入的参数个数。

注意空格会切割字符串,但是双引号可以拼接。

使用file命令查看Hello的格式,这是一个64位的可执行文件,供给x86使用。

用MobaX打开com4登录开发板。

ping通Ubuntu的桥接网卡,注意此时网线连接右边(电源接口朝上)靠近HDMI接口的网口,USB线连接下面的mircob接口。

在万物皆为文件的Linux系统中,访问其他设备上的数据的最常用方式就是挂载目录。

mount DeviceName BoradIndex

用无锁指令将网络设备(Ubuntu的桥接网卡)挂载到开发板上。

配置交叉编译链并且交叉编译Hello.c.

开发板也可以运行Hello程序了。

02_Hello程序的引申

只涉及APP与libc的开发是用户态开发,而一旦开始使用printf、open、read则代表进入了用户态。

第二章 GCC编译器的使用

01_GCC编译过程

         一个C++文件要经过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)等4步才能变成可执行文件。

由高级语言生成汇编码的过程叫编译,由汇编码生成机器码的过程叫汇编;调试过程中我们将机器码重新转换为汇编码的过程叫做反汇编。多段机器码组合成APP的过程叫做链接。

预处理是对源代码做文本层面的替换,例如头文件的包含、宏定义的展开、注释的删除等。

注意,预处理过程无法发现语法错误,报错是在编译过程中发生的。

02_GCC常用选项

  gcc常用指令

-c选项

我们之前常用的-o指令当然也可以处理多个源文件的编译:

gcc -o Test Hello.c Sub.c Control.c ...

但这样会引入一个新问题,如果我们需要频繁修改其中的一个源文件进行调试,但是每次都进行-o编译会导致每一个源文件都被编译一次,费时费力。因此我们可以额外添加一个-c,分别隔离成一个个.o文件,最后全部链接。

-I(大写i)选项

<>与""引用头文件的差异:

""仅代表在当前目录下查找头文件;<>代表在整个系统工具链中指定的目录来查找。

-I指定当前路径。

-L与-l选项

静态链接(生成的文件会占用较大内存)

可以把一些常用的(且无需频繁修改)接口封装压缩成静态库。

示例如下:

占用内存

动态链接

很抽象,没小多少,可能是因为现在的测试代码比较小。

使用动态链接库-l"RingBuffer(动态库名称,可以省略后缀.so和前缀lib)"并且-L指定当前路径下查找动态库。

注意使用动态库编译的可执行程序需要用这条指令指定动态库位置之后才能运行。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./

针对其他选项的简单介绍

使用示范:

第三章 Makefile的使用

01_Makefile要达到的效果

Makefile 是与 make 工具配套的构建脚本文件,核心作用是自动化管理项目的编译、链接、清理等构建流程。古早时期,Linux上面没有系统的可视化IDE,想要编程调试都依赖VIM+GCC,但是手动编译多文件项目时,需要敲冗长的命令;但有了Makefile之后就只需要一次定义规则。

以notepad形式打开keil工程也可以看到,其实keil工程的本质也是通过Makefile控制各个编译链接过程来实现的。当然下面这个是XML图像描述语言。

02_Makefile的引入及规则

 

Makefile在本讲的核心作用是提高系统编译效率,首先就是判断哪个源文件被修改,最常用的办法是比较最后修改时间。

简单的原理介绍如下图所示:

示例如下:(注意下面每行gcc之前都是tab键不是空格!)

test : TestA.o TestB.o
	gcc -o test TestA.o TestB.o

TestA.o : TestA.c
	gcc -c -o TestA.o TestA.c

TestB.o : TestB.c
	gcc -c -o TestB.o TestB.c

最后make命令就可以正常使用了:

03_Makefile的语法

$符号说明

常用符号

以上一讲我们谈到的gcc编译为例:

test : TestA.o TestB.o TestC.o
	gcc -o test $^

其中$^是Makefile的自动变量,代表所有依赖文件的列表。

%.o : %.c
	gcc -c -o $@ $<

第一行可以解释为:“目标文件 : 依赖项”,其中%是通配符,代表任意文件名。

第二行中,$@代表目标文件名,$<代表第一个依赖的文件名。

我们也可以新添加一个目标文件,名为clean,如下所示:

test : TestA.o TestB.o TestC.o
	gcc -o test $^

%.o : %.c
	gcc -c -o $@ $<

clean :
	rm *.o test	

如果我们直接使用make命令,那么生成目标缺省就是test;如果我们加上clean 选项,那么就会执行clean的依赖项。

假想目标

但是上述写法存在明显的缺陷,假如我们先用下面的命令创造一个名为“clean”的空文件:

> clean

通过这个命令发现一个AI的小bug,输入对话框的文本会自动被解析为Markdown格式,因此首个字符输入为>会被AI理解为块引用符号。

这时make clean失效了:

因为当前文件夹下已经有了一个clean文件,那么系统就会开始比较依赖项被修改的时间与clean的 生成时间,而咱们的clean在Makefile中没有依赖项,所以就会缺省忽略执行。

这时候就引入.PHONY关键字,另外提醒一下和我一样用VScode远程链接Ubuntu的同学,修改虚拟机里面的Makefile文件之后要及时保存。

test : TestA.o TestB.o TestC.o
	gcc -o test $^

%.o : %.c
	gcc -c -o $@ $<

clean :
	rm *.o test	

.PHONY : clean

两大变量

即时变量也可以叫简单变量。

使用示例:

A := abc # A的值即刻确定,在定义时就确定
B = 123  # B的值使用时才确定

all:          
	echo $(A)
	echo $(B)

/explain:

在终端中输入make回读:

这个example看不出咱们即时变量与延时变量的差异,可以修改一下代码:

A := $(c) # A的值即刻确定,在定义时就确定
B = $(c) # B的值使用时才确定
c = hello world

all:          
	@echo A = $(A)
	@echo B = $(B)

终端输出:

很明显,A是空行。

注意,make命令执行时会扫描全局,因此变量C的赋值位置关系不大 ,下面的示例输出结果不变。

A := $(C) # A的值即刻确定,在定义时就确定
B = $(C) # B的值使用时才确定
#C = hello world

all:          
	@echo A = $(A)
	@echo B = $(B)

C = hello world

并且和C语言不同的是,变量值在被使用时,默认使用最后一个覆盖上去的数值,而不是C语言那种串行执行的逻辑。

A := $(C) # A的值即刻确定,在定义时就确定
B = $(C) # B的值使用时才确定
C = 123

all:          
	@echo A = $(A)
	@echo B = $(B)

C = hello world

输出结果依旧是不变的。

变量的赋值方法很多,比如:= 、=、?=、+=等。

通过命令行也可以给变量赋值:

A := $(C) # A的值即刻确定,在定义时就确定
B = $(C) # B的值使用时才确定
C = 123
D ?= 456

all:          
	@echo A = $(A)
	@echo B = $(B)
	@echo D = $(D)

C = hello world

 侧面也在验证了?=的作用: 

04_Makefile函数

 概要如下

Foreach函数

$(foreach var, list, text)

它是Makefile的内建函数,主要的作用是文本处理,用于对列表中每个元素执行指定的操作。

参数说明:

①var:迭代变量名

②list:要遍历的列表

③text:对每个元素执行的表达式

使用示例:

A = a b c
B = $(foreach f, $(A), $(f).o)

all :
	@echo B = $(B) 

终端输出结果:

Filter函数

$(filter pattern...,text)  # 在text中取出符合pattern格式的值
$(filter-out pattern...,text)  # 在text中取出不符合pattern的值

使用示例:

C = a b c d/

D = $(filter %/,$(C))
E = $(filter-out %/,$(C))

all :
	@echo D = $(D)
	@echo E = $(E)

终端输出结果(忽略第一行):

wildcard函数

$(wildcard pattern)

# pattern定义了文件名的格式
# wildcard取出其中存在的文件

使用示例:

files=$(wildcard *.c)

# 从下面的一系列文件中选出真实存在文件夹中的文件
files2 = a.c b.c c.c d.c e.c
files2 = $(wildcard *.c)

输出结果:

patsubst函数

$(patsubst pattern,replacement,$(var))

 功能介绍:

05_Makefile实例

引入宏定义修改无法被编译器察觉的bug,复制03_TestApp到07_Example。

备份一下代码,TestB.c:

#include <stdio.h>

void func_B() {
    printf("Function B executed.\n");
    // Function implementation
}

TestA.c:

#include <stdio.h>

extern void func_B();
extern void func_C();

int main() {
    func_B();
    func_C();

    return 0;
}

Makefile:

test : TestA.o TestB.o TestC.o
	gcc -o test $^

%.o : %.c
	gcc -c -o $@ $<

clean :
	rm *.o test

.PHONY: clean

引入一个TestC.c:

#include <stdio.h>
#include "TestC.h"
void func_C() {
    printf("This is C = %d.\n",C);
    // Function implementation
}

和TestC.h文件:

#ifndef __TESTC_H__
#define __TESTC_H__

#define C 30

void func_C();

#endif // __TESTC_H__

修改TestA.c,执行TestC.c定义的函数:

#include <stdio.h>

extern void func_B();
extern void func_C();
int main() {
    func_B();
    func_C();

    return 0;
}

通过make命令编译并且查看输出结果:

修改C的宏定义为3,执行make发现编译器没有发现修改内容,预处理过程出错:

原因是makefile文件中规定.o只依赖于.c,头文件的更新不会影响.o文件。

test : TestA.o TestB.o TestC.o
	gcc -o test $^

%.o : %.c %.h
	gcc -c -o $@ $<

clean :
	rm *.o test

.PHONY: clean

可以通过下面的命令获得源文件需要的依赖:

 gcc -M ./TestC.c

效果如图所示:

把这些依赖项写进一个.d文件:

gcc -M -MF TestC.d TestC.c

catch一下具体内容:

 既生成目标文件也生成依赖文件:

gcc -c -o  TestC.o TestC.c  -MD -MF TestC.d

总结一下:

makefile:

objs = TestA.o TestB.o TestC.o

dep_files := $(patsubst %, .%.d, $(objs))    # 将目标文件列表转换为对应的依赖文件列表,格式为 .filename.d
dep_files := $(wildcard $(dep_files))        # 查找当前目录下实际存在的依赖文件

CFLAGS = -Werror                             # complie options: treat warnings as errors

test : $(objs)                               # 目标:编译生成可执行文件 test 依赖:所有目标文件
	gcc -o test $^
#	@echo dep_files = $(dep_files) 

ifneq ($(dep_files),)                        # 如果存在依赖文件,则包含它们
include $(dep_files)
endif

%.o : %.c                                    # 隐含规则:将 .c 源文件编译为目标文件 .o;同时生成对应的依赖文件,记录头文件依赖关系
	gcc $(CFLAGS) -c -o $@ $< -MD -MF .$@.d  
 
clean :
	rm *.o test

distclean :                                  # 清理依赖文件目标:删除所有自动生成的依赖文件
	rm $(dep_files)

.PHONY : clean

06 通用Makefile的使用

 韦东山老师参考Linux官方内核的Makefile给大家修改了一版本通用Makefile,本讲主要讲解下面这个Makefile的使用规则:


CROSS_COMPILE = 
AS		= $(CROSS_COMPILE)as
LD		= $(CROSS_COMPILE)ld
CC		= $(CROSS_COMPILE)gcc
CPP		= $(CC) -E
AR		= $(CROSS_COMPILE)ar
NM		= $(CROSS_COMPILE)nm

STRIP		= $(CROSS_COMPILE)strip
OBJCOPY		= $(CROSS_COMPILE)objcopy
OBJDUMP		= $(CROSS_COMPILE)objdump

export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include

LDFLAGS := 

export CFLAGS LDFLAGS

TOPDIR := $(shell pwd)
export TOPDIR

TARGET := test


obj-y += main.o
obj-y += sub.o
obj-y += a/


all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build

$(TARGET) : start_recursive_build
	$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

clean:
	rm -f $(shell find -name "*.o")
	rm -f $(TARGET)

distclean:
	rm -f $(shell find -name "*.o")
	rm -f $(shell find -name "*.d")
	rm -f $(TARGET)
	

 关键说明备份(Git上面下载的原版txt需要用GB2312编码打开):

本程序的Makefile分为3类:
1. 顶层目录的Makefile
2. 顶层目录的Makefile.build
3. 各级子目录的Makefile

一、各级子目录的Makefile:
   它最简单,形式如下:

   EXTRA CFLAGS  :=
   CFLAGS_file.o :=

   obj-y += file.o     
   obj-y += subdir/

   "obj-y += file.o"     //表示把当前目录下的file.c编进程序里,
   "obj-y+=subdir/"      //表示要进入subdir这个子目录下去寻找文件来编进程序里,是哪些文件由subdir目录下的Makefile决定。
   "EXTRA CFLAGS",       //它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项,可以不设置
   "CFLAGS_xxx.O",       //它给当前目录下的xxx.c设置它自己的编译选项,可以不设置

注意:
1. "subdir/"中的斜杠"/"不可省略
2. 顶层Makefile中的CFLAGS在编译任意一个.c文件时都会使用
3. CFLAGS EXTRA_CFLAGS CFLAGS_xxx.o 三者组成xxx.c的编译选项

二、顶层目录的Makefile:
   它除了定义obj-y来指定根目录下要编进程序去的文件、子目录外,
   主要是定义工具链前缀CROSS_COMPILE,
   定义编译参数CFLAGS,
   定义链接参数LDFLAGS,
   这些参数就是文件中用export导出的各变量。

三、顶层目录的Makefile.build:
   这是最复杂的部分,它的功能就是把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,打包为built-in.o
   详细的讲解请看视频。

四、怎么使用这套Makefile:
   1. 把顶层Makefile,Makefile.build放入程序的顶层目录
   在各自子目录创建一个空白的Makefile

   2. 确定编译哪些源文件
   修改顶层目录和各自子目录Makefile的obj-y:
   obj-y += xxx.o
   obj-y += yyy/
   这表示要编译当前目录下的xxx.c,要编译当前目录下的yyy子目录

   3. 确定编译选项、链接选项
   修改顶层目录Makefile的CFLAGS,这是编译所有.c文件时都要用的编译选项;
   修改顶层目录Makefile的LDFLAGS,这是链接最后的应用程序时的链接选项;

   修改各自子目录下的Makefile:
   "EXTRA CFLAGS",它给当前目录下的所有文件(不含其下的子目录)设置额外的编译选项,可以不设置
   "CFLAGS XXX.O",它给当前目录下的xxx.c设置它自己的编译选项,可以不设置  
    
   4. 使用哪个编译器?
   修改顶层目录Makefile的CROSS_COMPILE,用来指定工具链的前缀(比如arm-linux-)

   5. 确定应用程序的名字:
   修改顶层目录Makefile的TARGET,这是用来指定编译出来的程序的名字

   6.执行“make”来编译,执行“make clean”来清除,执行“make distclean”来彻底清除

这个通用Makefile使用之前对咱们的工程文件框架有一定的要求,简单来说,在大的文件夹下,可以包含若干个源文件(例如main.c、kernel.c等),然后有一个总的Makefile和Makefile.build文件对整个项目的编译规则进行约束;紧接着,在总文件下,可能存在拥有源文件的子文件夹(例如BSP组件包),这个子文件夹也需要Makefile说明编译需要的依赖项;最后,用一个Include文件夹包含所有头文件。

我模仿设备树diagram画了个草图更加直观:

下面的两个操作可以定义源文件中的宏:

EXTRA_CFLAGS := -D DEBUG
CFLAGS_sub3.o := -D DEBUG_SUB3

源文件:

#include <stdio.h>
#include <sub3.h>

void sub3_fun(void)
{
    printf("Sub3 fun, C = %d!\n", C);
	
#ifdef DEBUG
		printf("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
#endif

#ifdef DEBUG_SUB3
		printf("It is only debug info for sub3.\n");
#endif

}

在这个工程中,你无法用编辑器找到DEBUG的定义,因为它是由Makefile控制的。

但是在编译过程中可以看到DEBUG被引入了:

也可以看到不同的sub文件对不同的宏有依赖。

最后感谢韦老师的开源,这个通用的Makefile是他用爱发电公布的。

07 通用Makefile的解析

make命令的使用:

通用Makefile的设计思想

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐