通过构建一个简单的项目来理解 C/C++ 构建系统,包括“memory_api.h”应该是#include“free_memory_api.h”,以便教程按预期工作,我等不及第二部分了;)

2025-06-07

通过构建简单项目来理解 C/C++ 构建系统

为了使教程能够按预期工作,包括“memory_api.h”应该是#include“free_memory_api.h”,我迫不及待地想看第二部分;)

C/C++ 是当今许多流行编程语言的鼻祖,我们今天使用的所有高级编程语言,如 Python、JavaScript,都是使用 C/C++ 构建的。例如,标准的 Python 解释器CPython是使用 C 构建的,而最流行的 JavaScript 实现V8是使用 C/C++ 构建的,C/C++ 还为 Node.js 使用的大多数底层库提供支持,换句话说,C/C++ 为人类编写的大多数开源软件提供支持。我们更喜欢 Python 等高级语言的主要原因之一是它们提供了强大的包管理工具,我们不必再担心管理依赖项,它们会pip自动为我们管理。JavaScript 也是如此。这些语言还具有强大的构建系统,使我们能够更轻松地构建和发布软件。

C/C++ 也有一些流行的构建系统,例如cmake和,bazel它们可以自动管理依赖项,但在本文中,我们将在不使用这些工具的情况下编译一个 C/C++ 项目,以便了解其内部工作原理。

我们将构建一个简单的系统记录器,每 5 秒记录一次系统的总可用 RAM 内存。

首先!让我们创建一个项目结构:

项目结构必须易于理解,并应尽可能隔离不同的功能,以避免混淆。没有人会阻止我们使用自己的项目结构,但大多数使用 C/C++ 构建的开源项目都使用这种结构:

project_root
  - include
  - src
      - module-1
      - module-2
      - module-n
      - main.c/main.cc (depends on the project)
  - Makefile
  - README
  - LICENSE
  - misc files
Enter fullscreen mode Exit fullscreen mode

让我们看看每个文件/目录的含义:

  1. include- 这是我们所有头文件所在的地方。
  2. src- 包含所有源代码的目录。src 目录下可以包含多个子目录/模块。此外,还可以包含一个主函数文件src
  3. Makefile:Makefile 由命令使用make,我们将使用它make来构建我们的项目。

对于我们的项目,我们将具有以下结构:

memlogger
   - bin  - will explain the need for this
   - include
       - free_memory_api.h
       - file_writer.h
   - src
       - free_memory_api
          - free_memory_api.c
       - file_writer
          - file_writer.c
       - main.c
   - Makefile
Enter fullscreen mode Exit fullscreen mode

让我们编码

完成项目结构设置后,我们就可以开始编写代码了。为了避免篇幅过长,我不会深入解释代码,而是会更多地关注概念。

什么是头文件?

头文件是我们实际 C 代码的蓝图。对于我们编写的每个 C 模块,导出头文件是一个好习惯。编译器使用这些头文件来了解模块导出的所有函数。编译完成后,头文件就不会再被使用。当我们的项目/模块被其他项目用作模块时,头文件的实际用途就显现出来了,其他程序员只需包含我们的头文件即可使用我们导出的函数声明。

让我们free_memory_api.h按照结构创建:

#ifndef __FREE_MEMORY_API
#define __FREE_MEMORY_API
//this is our API function which returns free memory in bytes
unsigned long long get_free_system_memory();
#endif
Enter fullscreen mode Exit fullscreen mode

让我们创建file_writer.h声明文件写入 API

#ifndef __FILE_WRITER_API
#define __FILE_WRITER_API

#include <stdio.h>
//opens the log file for writing
FILE * open_log_file(char * path);
// we will use this function to write contents to the log file
void write_log_to_file(FILE * file, unsigned long long free_memory);
//closes the log file
void close_log_file(FILE * file);
#endif
Enter fullscreen mode Exit fullscreen mode

让我们定义这些 API:

我们声明了所有需要的 API,但尚未编写这些 API 的底层代码。我们将编写文件日志记录和从系统获取可用内存的代码。在编写代码之前,我们必须导入之前声明的蓝图。
file_writer.c

#include "file_writer.h"

// Open the log-file in append mode and return it
FILE * open_log_file(char * file_path) {
    FILE * fp = fopen(file_path, "a");
    return fp;
}

// Close the file 
void close_log_file(FILE * fp) {
    if(fp) {
        fclose(fp);
    }
}

//write log entry into the file
void write_log_to_file(FILE * fp, unsigned long long free_memory) {
    if(fp) {
        fprintf(fp, "free_memory=%llu\n", free_memory);
    }
}
Enter fullscreen mode Exit fullscreen mode

现在让我们定义免费内存 API,即free_memory_api.c

#include <sys/sysinfo.h>
#include "free_memory_api.h"

unsigned long long get_free_system_memory() {
    struct sysinfo info;
    if (sysinfo(&info) < 0) {
         return 0;
    }
    return info.freeram;;
}
Enter fullscreen mode Exit fullscreen mode

最后,main.c

#include "file_writer.h"
#include "free_memory_api.h"

#include <unistd.h>

int main(int argc, char **argv) {
    if (argc < 2) {
        printf("Provide log file name\n");
        return 0;
    } 

    unsigned long long free_memory = 0;
    while(1) {
        free_memory = get_free_system_memory();
        FILE * log = open_log_file(argv[1]);
        write_log_to_file(log, free_memory);
        close_log_file(log);
        sleep(5);
    }
}
Enter fullscreen mode Exit fullscreen mode

让我们开始构建项目

代码编写完毕后,我们就可以编译项目了。现在我们的项目中有多个模块。这些模块可以链接在一起构建一个独立的可执行文件,也可以将各个模块单独构建为共享库,然后在运行时将它们链接在一起。

构建静态单片可执行文件

在本节中,我们将构建一个可以交付的二进制文件。构建 C/C++ 项目的方法有很多种。在本文中,我们将仅构建一个独立的可执行文件,这是构建 C 项目最简单的方法。
我们将使用make一个可以自动执行任何任务的 Linux 命令行实用程序,它是一系列 Shell 命令,可以分组并标记为特定任务。我们可以使用 方便地编写多个这样的任务Makefile。现在让我们看看我们的 Makefile。

COMPILER=gcc

file_writer:
    @$(COMPILER) -c src/file_writer/*.c -Iinclude/ -o bin/file_writer.o
    @echo "Built file_writer.o"

free_memory_api:
    @$(COMPILER) -c src/free_memory_api/*.c -Iinclude/ -o bin/free_memory_api.o 
    @echo "Built free_memory_api.o"

project:
    $(COMPILER) -c src/main.c -Iinclude/ -o bin/main.o
    @$(COMPILER) bin/free_memory_api.o bin/file_writer.o bin/main.o -o memlogger
    @echo "Finished building memlogger"
Enter fullscreen mode Exit fullscreen mode

尽管我们可以用一个命令构建整个项目,但我将其分为三个阶段。

  1. file_writer:此make规则将file_writer.o在下生成目标文件./bin
  2. free_memory_api:此规则free_memory_api.o在下生成./bin
  3. project:这将构建整个项目,它生成main.o并链接main.o其他两个目标文件以创建一个名为的独立可执行文件memlogger

让我们用 make 执行这些命令:
步骤 1。file_writer.o

   make file_writer
Enter fullscreen mode Exit fullscreen mode

步骤2。free_memory_api.o

   make free_memory_api
Enter fullscreen mode Exit fullscreen mode

步骤3. 最终二进制文件:

   make project
Enter fullscreen mode Exit fullscreen mode

执行二进制文件:

我们可以像运行普通 Linux 可执行文件一样运行该二进制文件:

memlogger logs.txt
Enter fullscreen mode Exit fullscreen mode

15 秒后,我们在日志文件中看到 3 个条目:

free_memory=8322523136
free_memory=8330776576
free_memory=8335728640
Enter fullscreen mode Exit fullscreen mode

这大约相当于8GB释放了16GBRAM,而且是正确的。好极了!我们创建了一个小型系统记录器。

引擎盖下发生了什么?

理解我们这里使用的构建过程非常重要。为了理解这一点,我们需要了解目标文件的概念。

我们在 Makefile 中做了什么?

我们定义了三条规则,每条规则构建一个模块,第三条规则进一步链接所有三个模块。
我们使用了gcc编译器(g++适用于 C++ 项目)。使用的选项:

  1. -c:这告诉编译器只编译而不执行链接,因为我们在步骤 3 中明确链接目标文件。
  2. -I:由于我们定义了自己的标头,因此我们必须在编译时将其提供给编译器,默认情况下,编译器会在标准位置搜索这些标头,我们还可以告诉编译器包含我们的自定义位置以使用来解析标头-I
  3. -o:输出文件名。

什么是目标文件?

目标文件是ELF - Executable and linkable format由编译器生成的 Linux 二进制文件。ELF 格式遵循 POSIX 标准,所有 Linux 发行版都能理解什么是目标文件。通俗地说,目标文件包含一个映射表和符号定义。

  1. mapping-table or Symbol table:映射表包含由目标文件定义的一组符号和定义符号实际代码的文本段中的偏移量。
  2. Symbol definitions:此段包含所有函数的机器码。因此,为了获取函数/符号的机器码,我们需要执行以下两个步骤:首先,查找目标文件的符号表,获取其在文本段中的偏移量。然后,进入文本段并获取其代码。

这些只是外行术语,ELF 有标准定义,mapping-table对于Segment definitions初学者来说可能比较困惑。

让我们看一下目标文件的内容file_writer.o,我们使用一个名为的工具,readelf它是所有 Linux 系统中的默认工具。

readelf -h bin/file_writer.o
Enter fullscreen mode Exit fullscreen mode

输出:

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1168 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 12
Enter fullscreen mode Exit fullscreen mode

这些是 ELF 头文件。现在我们来看一下符号表(或者用我们的话来说就是映射表)。

readelf --syms bin/file_writer.o
Enter fullscreen mode Exit fullscreen mode

输出:

Symbol table '.symtab' contains 16 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS file_writer.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000000    41 FUNC    GLOBAL DEFAULT    1 open_log_file
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND fopen
    12: 0000000000000029    34 FUNC    GLOBAL DEFAULT    1 close_log_file
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND fclose
    14: 000000000000004b    54 FUNC    GLOBAL DEFAULT    1 write_log_to_file
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND fprintf
Enter fullscreen mode Exit fullscreen mode

如表所示,表中有open_log_file、的条目close_log_filewrite_log_to_file它们是我们定义的 API 函数。太好了!我们的目标文件正确无误。此外,如果仔细观察,我们会看到fclosefopen和的存在,fprintf并且它们带有前缀,UND这意味着这些符号地址尚不清楚,但 C/C++ 运行时会在链接过程中(即步骤 3)解析它们,它会在运行时静态或动态地链接到这些函数。我们将在下一部分中了解共享库的概念。

类似地,我们可以运行相同的命令来free_memory_api.o查看它的符号表。我们得到如下输出:

Symbol table '.symtab' contains 14 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS free_memory_api.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000000    89 FUNC    GLOBAL DEFAULT    1 get_free_system_memory
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND sysinfo
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __stack_chk_fail
Enter fullscreen mode Exit fullscreen mode

我们可以将get_free_system_memoryaFUNC看作sysinfo是未定义的。

我们最后一步做了什么?

在前两步中,我们编译了模块并生成了目标文件,但它们无法执行,因为它们没有main函数定义,而函数定义是任何 C/C++ 程序的入口点。我们在规则(步骤 3)中有两个命令Makefileproject第一个命令只将 main.c 文件编译成main.o,让我们尝试运行它,它应该可以运行,因为它有main函数。

./main.o
Enter fullscreen mode Exit fullscreen mode

输出:

bash: ./bin/main.o: cannot execute binary file: Exec format error
Enter fullscreen mode Exit fullscreen mode

我们无法运行它,因为它不是可执行文件,而是一个目标文件。最后一步是链接所有三个目标文件并生成最终的可执行二进制文件。
在此之前,我们将尝试仅链接main.o并丢弃剩下的两个模块,让我们看看会发生什么:

gcc bin/main.o -o memlogger
Enter fullscreen mode Exit fullscreen mode

输出:

bin/main.o: In function `main':
main.c:(.text+0x36): undefined reference to `get_free_system_memory'
main.c:(.text+0x4d): undefined reference to `open_log_file'
main.c:(.text+0x64): undefined reference to `write_log_to_file'
main.c:(.text+0x70): undefined reference to `close_log_file'
collect2: error: ld returned 1 exit status
Enter fullscreen mode Exit fullscreen mode

这正是我们预期会发生的情况,可执行文件需要以下函数,但不知道它们在哪里。所以我们需要将它与剩下的两个目标文件链接起来。

gcc bin/free_memory_api.o bin/file_writer.o bin/main.o -o memlogger
Enter fullscreen mode Exit fullscreen mode

现在,编译器会检查file_writer.o和的符号表free_memory_api.o,以解析上一个命令中未定义的函数。由于这两个目标文件的符号表定义了这些符号/函数,因此链接成功,并生成了最终的可执行文件。

让我们看看二进制文件的映射表或符号表memlogger

readelf --syms memlogger
Enter fullscreen mode Exit fullscreen mode

输出:

    26: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    27: 0000000000000780     0 FUNC    LOCAL  DEFAULT   14 deregister_tm_clones
    28: 00000000000007c0     0 FUNC    LOCAL  DEFAULT   14 register_tm_clones
    29: 0000000000000810     0 FUNC    LOCAL  DEFAULT   14 __do_global_dtors_aux
    30: 0000000000201010     1 OBJECT  LOCAL  DEFAULT   24 completed.7698
    31: 0000000000200d88     0 OBJECT  LOCAL  DEFAULT   20 __do_global_dtors_aux_fin
    32: 0000000000000850     0 FUNC    LOCAL  DEFAULT   14 frame_dummy
    33: 0000000000200d80     0 OBJECT  LOCAL  DEFAULT   19 __frame_dummy_init_array_
    34: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS free_memory_api.c
    35: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS file_writer.c
    36: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
    37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    38: 0000000000000c5c     0 OBJECT  LOCAL  DEFAULT   18 __FRAME_END__
    39: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    40: 0000000000200d88     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_end
    41: 0000000000200d90     0 OBJECT  LOCAL  DEFAULT   21 _DYNAMIC
    42: 0000000000200d80     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_start
    43: 0000000000000a78     0 NOTYPE  LOCAL  DEFAULT   17 __GNU_EH_FRAME_HDR
    44: 0000000000200f80     0 OBJECT  LOCAL  DEFAULT   22 _GLOBAL_OFFSET_TABLE_
    45: 0000000000000a30     2 FUNC    GLOBAL DEFAULT   14 __libc_csu_fini
    46: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
    47: 0000000000201000     0 NOTYPE  WEAK   DEFAULT   23 data_start
    48: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.5
    49: 0000000000201010     0 NOTYPE  GLOBAL DEFAULT   23 _edata
    50: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fclose@@GLIBC_2.2.5
    51: 0000000000000a34     0 FUNC    GLOBAL DEFAULT   15 _fini
    52: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2
    53: 00000000000008dc    34 FUNC    GLOBAL DEFAULT   14 close_log_file
    54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5
    55: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    56: 0000000000201000     0 NOTYPE  GLOBAL DEFAULT   23 __data_start
    57: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fprintf@@GLIBC_2.2.5
    58: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    59: 0000000000201008     0 OBJECT  GLOBAL HIDDEN    23 __dso_handle
    60: 0000000000000a40     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    61: 000000000000085a    89 FUNC    GLOBAL DEFAULT   14 get_free_system_memory
    62: 00000000000009c0   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
    63: 0000000000201018     0 NOTYPE  GLOBAL DEFAULT   24 _end
    64: 0000000000000750    43 FUNC    GLOBAL DEFAULT   14 _start
    65: 0000000000201010     0 NOTYPE  GLOBAL DEFAULT   24 __bss_start
    66: 0000000000000934   130 FUNC    GLOBAL DEFAULT   14 main
    67: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fopen@@GLIBC_2.2.5
    68: 00000000000008fe    54 FUNC    GLOBAL DEFAULT   14 write_log_to_file
    69: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sysinfo@@GLIBC_2.2.5
    70: 0000000000201010     0 OBJECT  GLOBAL HIDDEN    23 __TMC_END__
    71: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    72: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@@GLIBC_2.2.5
    73: 00000000000008b3    41 FUNC    GLOBAL DEFAULT   14 open_log_file
    74: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@@GLIBC_2.2
    75: 0000000000000698     0 FUNC    GLOBAL DEFAULT   11 _init
Enter fullscreen mode Exit fullscreen mode

由于符号表很大,我从第 26 条开始粘贴条目。正如你所见,我们最终的可执行文件包含了所有三个模块的定义,而且没有UND符号,这些符号现在被替换成了类似 的内容fopen@@GLIBC_2.2.5。这意味着,这些函数的代码不会被复制到我们的二进制文件中,而是必须在运行时解析,Linux 加载器负责ld.so在运行时动态链接这些符号。

就这样!这篇文章的第一部分已经完成了。如果你正在读这行字,我由衷地感谢你读完整篇文章,即使你跳过了所有内容直接跳到这里,也请保留我的赞美。

谢谢:)祝您玩得愉快。

文章来源:https://dev.to/narasimha1997/understanding-cc-build-system-by-building-a-simple-project-part-1-4fff
PREV
😎 React App 通过开源 SSO Auth Wizardry 升级 🪄
NEXT
如何创建和发布 npm 模块