Organizing Code with Makefile

Makefile

A Makefile is a text file that contains a series of rules to tell the build tool make how to build a software project. It describes the dependencies between each source file in the project and specifies how to compile the source files into target files, as well as how to link these target files to generate the final executable or library files.

Contents

  • Variable Definitions: A series of variables are defined to store information such as the compiler, compilation options, and target file names.

  • Rule Definitions: Each rule includes the target file, dependency files, and execution commands. The make tool uses these rules to determine which files need to be recompiled and which files are up to date and do not need recompilation.

  • Comments: Makefiles support comments for explaining and clarifying the contents within the Makefile.

  • Built-in Functions and Directives: Some built-in functions and directives can be used in Makefiles to manipulate variables, execute commands, and perform conditional checks. These functions and directives provide rich functionality for conveniently writing complex build rules.

Basic Syntax

target: dependencies
	command
  • target: The name of the target file to be generated.

  • dependencies: A list of files that the target file depends on.

  • command: The command to generate the target file.

Variable Definition

CC = gcc 
CFLAGS = -Wall
target: dependencies
	$(CC) $(CFLAGS) -o target dependencies

Wildcard

sources = $(wildcard *.c)
objects = $(sources:.c=.o)
target: $(objects)
	$(CC) $(CFLAGS) -o target $(objects)

Phony Target

.PHONY: clean
clean:
	rm -f target *.o
  • .PHONY: Indicates a phony target, which is not a real file, telling make not to look for this filename.

Automatic Variables

objects = $(sources:.c=.o)
target: $(objects)
	$(CC) $(CFLAGS) -o target $^
  • $^: A list of all dependency files.

Conditional Statements

DEBUG = 1
ifeq ($(DEBUG), 1)
	CFLAGS += -g
else
	CFLAGS += -O2
endif
  • ifeq: Executes if the condition is true.

Include and Export

include config.mk
  • include: Includes other Makefiles.

  • Used in Makefiles to pass configuration options to the called sub-Makefile or other commands, allowing the subprocess to use that variable.

Compilation Options Variables

CFLAGS = #-c     # Compilation options variable
CFLAGS  += #-g #-Wall -fno-stack-protector -rdynamic -g 
# -g generates debug information that can be used by the debugger for source-level debugging. This is very useful for identifying and fixing errors during development.
# -Wall: Enables all common compiler warnings. This helps to discover potential code issues or non-standard writing.
# -fno-stack-protector: Disables stack protection. Stack protection is a security feature used to detect stack overflow attacks.
# -rdynamic: Generates a loadable dynamic link library (shared object), allowing symbols in the dynamic link library to be resolved at runtime. This is useful in some dynamically loaded module scenarios.

CFLAGS  += #-I$(LIBMTN_CFG_PATH)/include
# -I: This is a compiler option used to specify the search path for header files. Here, the path to the header files follows -I. Spaces can be present between the -I option and the header file path.

CFLAGSL += #-L$(LIBMTN_PATH)
# -L: This is a compiler option used to specify the search path for library files. Here, the path to the library files follows -L.

LDLIBS  += #-lpthread
# Used to specify the libraries needed during linking. Similar to CFLAGS and LDFLAGS, LDLIBS allows you to set options during the linking phase.

LDFLAGS += #-L$(ROOTDIR)/lib/$(openssl_dir) -lssl -lcrypto -lcrypt
# -L$(ROOTDIR)/lib/$(openssl_dir): This is a compiler option used to specify the search path for library files during linking. Here, the path follows -L.
# -lssl -lcrypto -lcrypt: This is a compiler option used to specify the libraries during linking. Here, -l is followed by the library names, and the compiler will search for and link these libraries in the specified search paths.
# Specifically, -lssl indicates linking the OpenSSL SSL library, -lcrypto indicates linking the OpenSSL Crypto library, and -lcrypt indicates linking the system's cryptographic library.
# LDFLAGS: This is a variable in a Makefile used to specify options during the linking phase. Similar to CFLAGS, LDFLAGS allows you to set options for linking.

Linking (Soft and Hard): ln -snf /tmp/serverkey.pem …/…/romfs/etc/serverkey.pem

Using for and if (demo): Polling subpaths and performing automatic compilation #Method 1

for target in $(SUBDIRS); do 
	echo "Building $(SUBDIRS)..."; 
	$(MAKE) -C $$target; 
done

#Method 2: Polling subpaths to check if Makefile exists

if [ -f $@/Makefile ]; then 
	$(MAKE) -C $@ ; 
	$(CC) main.c -o main.o; 
else 
	echo "No Makefile in $@, skipping..."; 
echo "No Makefile in $@, skipping..."; 
fi

Makefile Organization Example

Use the tree command to view the code structure:

├── char_driver
│   ├── helloDev.c
│   ├── Makefile
│   └── test.c
├── char_ioctl
│   ├── char_ioctl_driver.c
│   ├── char_ioctl_user
│   ├── char_ioctl_user.c
│   └── Makefile
├── dns_src
│   ├── dns.c
│   ├── dns_excute.c
│   └── Makefile
├── GDB_debug
│   ├── backtrace.c
│   ├── gdb_server_proxy.c
│   ├── hello.c
│   ├── Makefile
│   ├── multiprocess_demo_2.c
│   ├── multiprocess_demo.c
│   ├── multithread_demo.c
│   └── Segmentation_fault.c
├── hook_fuction
│   ├── hook.c
│   └── Makefile
├── ifconfig_achieve
│   ├── ifconfig_1.c
│   ├── ifconfig_wlan_information.c
│   ├── ifconfig_wlan_ip.c
│   └── Makefile
├── ip_addr_exchange
│   ├── ip_addr_exchange.c
│   └── Makefile
├── main.c
├── Makefil
├── Makefile
├── mk_test
├── net_device_driver
│   ├── 1111111.c
│   ├── Makefile
│   ├── net_device.c
│   ├── net_device_dirver.c
│   ├── net_device_tun_driver.c
│   ├── net_device_tun_user_tx.c
│   ├── net_device_user1
│   ├── net_device_user2
│   ├── net_device_user2.c
│   ├── net_device_user.c
│   ├── net_device_user_tap
│   ├── net_device_user_tap.c
│   ├── net_device_user_tun.c
│   ├── openvpn-as_2.12.0-2e834031-Ubuntu20_amd64.deb
│   ├── tap_demo
│   ├── tap_demo1
│   ├── tap_demo1.c
│   ├── tap_demo.c
│   ├── tcpdump1.pcap
│   ├── tcpdump2.pcap
│   ├── tcpdump3.pcap
│   └── tcpdump4.pcap
├── net_device_driver2
│   ├── Makefile
│   ├── net_device_tun_driver.c
│   ├── net_device_tun_user
│   ├── net_device_tun_user.c
│   ├── tcpdump1.pcap
│   ├── tcpdump2.pcap
│   ├── tcpdump3.pcap
│   └── tcpdump.pcap
├── netlink_socket
│   ├── Makefile
│   ├── Module.symvers
│   ├── netlink_socket_dirver.c
│   └── netlink_socket_user.c
├── ping
│   ├── all
│   ├── Makefile
│   ├── ping.h
│   └── ping_system.c
├── ping_combin_bag.c
├── process_communication
│   ├── Makefile
│   ├── name_pipe_1.c
│   ├── name_pipe.c
│   ├── pipe.c
│   ├── queue_1.c
│   ├── queue.c
│   ├── semaphores_consumer.c
│   ├── semaphores_producer.c
│   ├── shared_memory_modify.c
│   ├── shared_memory_touch.c
│   ├── signals.c
│   └── signals_send.c
├── str_function
│   ├── Makefile
│   ├── str_function.c
│   └── str_function.h
├── tcp_test
│   ├── Makefile
│   ├── tcp_client.c
│   └── tcp_sever.c
└── udp_test
    ├── Makefile
    ├── udp_client.c
    └── udp_server.c

Although there is a lot of code above, it is mainly divided into two parts:

  1. Pure Application Layer Code

  2. Driver Code and Combined Code with Application Layer

Driver and Application Layer Combined Code Makefile: char_driver

CC = gcc
CFLAGS = -o

ifneq ($(KERNELRELEASE),)
	obj-m := helloDev.o
else
	PWD := $(shell pwd)
	#KDIR:= /lib/modules/4.4.0-31-generic/build
	KDIR := /lib/modules/`uname -r`/build
	all:
		make -C $(KDIR) M=$(PWD)
		$(CC) test.c $(CFLAGS) test.o
clean:
		rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~ *.a *.cmd* *.order
endif

Pure Application Layer Code: Root Directory Makefile Example

SUBDIRS := char_driver GDB_debug hook_fuction char_ioctl dns_src ifconfig_achieve ip_addr_exchange net_device_driver \
 net_device_driver2 netlink_socket process_communication udp_test tcp_test ping str_function
MAKE = make
CC = gcc
C = -c
.PHONY: all $(SUBDIRS)

all: $(SUBDIRS)
# Here $@ refers to the target of the current rule, i.e., the name of the subdirectory.
#Method 1
$(SUBDIRS):
	if [ -f $@/Makefile ]; then \
		$(MAKE) -C $@ ; \
		$(CC) main.c -o main.o; \
	else \
		echo "No Makefile in $@, skipping..."; \
		echo "No Makefile in $@, skipping..."; \
	fi
# File creation usage
	mkdir -p ./mk_test || exit $?;
	cp ./main.o ./mk_test/ || exit $?;
# Create a link when we need to use the same file in different directories, we don’t need to put a must-have identical file in every required directory,
# we just need to place it in a fixed directory and use the ln command to link it in other directories (link), without occupying disk space repeatedly.
# ln -snf /tmp/serverkey.pem ../../romfs/etc/serverkey.pem

#Method 2
# for target in $(SUBDIRS); do \
#     echo "Building $(SUBDIRS)..."; \
#     $(MAKE) -C $$target; \
# done

clean:
	for dir in $(SUBDIRS); do \
		if [ -f $$dir/Makefile ]; then \
			$(MAKE) -C $$dir clean; \
		else \
			echo "No Makefile in $$dir, skipping..."; \
		fi \
	done
	rm -rf ./mk_test/main.o

The above Makefile explanation: Method 1 and Method 2 use if and for statements respectively to achieve the same effect; you can uncomment whichever you want to compile.

Questions

$$target, can it be written as $$(target)?
In a Makefile, $$ is used to escape the $ symbol to prevent it from being interpreted as a Makefile variable or command-line variable.
Therefore, in the for loop, $$target is the correct way to reference the variable in the shell.
If written as $$(target), then Make will interpret $(target) as a Makefile variable rather than a shell variable.
So the correct way is $$target so that the shell can correctly interpret and execute the loop.

Leave a Comment