C语言
文章平均质量分 93
_Rye_
左手代码右手诗
一行代码一行诗
展开
-
31|程序如何与操作系统交互?
不知道还记不记得在 11 讲 “用于低级 IO 接口的操作系统调用”小节中给出的例子,通过这个例子我们能够发现,操作系统调用实际上是由操作系统内核封装好的一些可供上层应用程序使用的接口。这些接口为应用提供了可以按照一定规则访问计算机底层软件与硬件相关服务(如 IO、进程、摄像头等)的能力。其中,内核作为中间层,隔离了用户代码与硬件体系。接下来,再通过一个简单的例子,来快速回顾下如何在 x86-64 平台上使用系统调用。原创 2023-10-05 00:33:42 · 135 阅读 · 0 评论 -
30|ABI 与 API 究竟有什么区别?
今天,主要讲解了 API 与 ABI 这两个概念之间的区别。API 的全称为“应用程序编程接口”,它是程序员可以通过编程语言调用的一种特殊资源。API 通过隐藏功能的内部实现细节隔离了其使用方与提供方,使得 API 实现与应用程序实现可以通过遵循统一、稳定的 API 规范,来达到各自独立维护的目的。API 有着多种具体表现形式,比如源代码形式的函数,或是基于互联网的 Web 接口。原创 2023-10-05 00:04:32 · 790 阅读 · 0 评论 -
29|C 程序的入口真的是 main 函数吗?
到这里,已经把 _start 的由来和作用这两个关键问题弄清楚了,我想你已经知道了 main 函数究竟是如何被调用的。最后再来看一个问题:在上面提到了 C 运行时库,即 CRT,那么它究竟是什么呢?实际上,CRT 为应用程序提供了对启动与退出、C 标准库函数、IO、堆、C 语言特殊实现、调试等多方面功能的实现和支持。CRT 的实现是平台相关的,它与具体操作系统结合得非常紧密。当然,真正参与到 CRT 功能实现的并不只有 crt1.o 这一个对象文件。原创 2023-10-04 16:32:11 · 557 阅读 · 0 评论 -
28|程序可以在运行时进行链接吗?
这一讲,主要介绍了动态链接的基本实现方式,和基于此进行的加载时链接与运行时链接这两者的主要区别。动态链接利用 GOT,将需要重定位的部分,分离到所在进程的 Data Segment,进而使得共享库文件可以被加载到进程 VAS 中的任意位置。在这种情况下,多个进程便能够做到真正地共享同一段物理内存中的共享库代码。而为了降低程序初次执行时,大量符号重定位带来的性能损耗,编译器又利用名为 PLT 的表结构,实现了对函数符号的延迟绑定。加载时链接,是指在程序被真正执行前,动态链接器会首先完成对符号的重定位过程。原创 2023-10-04 15:56:24 · 90 阅读 · 0 评论 -
27|编译器在链接程序时发生了什么?
今天主要介绍了 Linux 中的静态链接,以此为例,深入了解了编译器在链接程序时发生了什么。静态链接与动态链接相对应,主要是指“在链接过程中,来自于不同目标文件的代码会被整合为二进制可执行文件的一部分”这个过程。总的来看,静态链接被分为两个步骤:符号解析与重定位。其中,符号解析是指为应用程序使用的所有符号正确匹配对应符号定义的过程。当有重名的多个符号定义存在时,链接器会按照一定规则来选择适用的版本。原创 2023-10-04 14:00:45 · 111 阅读 · 0 评论 -
26|进程是如何使用操作系统内存的?
这一讲,主要为你介绍了进程是如何在操作系统的控制下使用内存资源的。在现代计算机中,内存和 CPU 芯片上的高速缓存一起构成了用于承载应用运行时数据的缓存系统。而这个缓存系统,在名为“虚拟内存”机制的帮助下,能够以一种更加优雅的方式运作。虚拟内存机制为每一个进程都抽象出了独立且私有的虚拟地址空间(VAS)。VAS 中使用虚拟地址进行寻址,当 CPU 需要通过该地址访问内存中的某个数据时,芯片上的内存管理单元(MMU)会将该地址转换为对应的物理地址。原创 2023-10-04 00:42:34 · 220 阅读 · 0 评论 -
25|可执行二进制文件里有什么?
这一讲的内容主要是以可执行二进制文件作为切入点的。首先介绍了在不同操作系统上的几种常见可执行文件格式,然后以最常见的 ELF 格式为例,对它的组成细节进行了更为深入的探究。ELF 文件格式的基本组成结构可以被划分为 ELF 头、Section 和 Segment 三大主要部分。其中,各个 Section 中包含有按照功能类别划分好的、用于支撑 ELF 功能的各类数据。这些数据共同组成了 ELF 文件的静态视图,以用于支持 ELF 文件的链接过程。原创 2023-10-03 23:51:40 · 450 阅读 · 0 评论 -
24|实战项目(下):一个简单的高性能 HTTP Server
在这一讲中,从基本的项目目录创建,到模块功能编写,再到代码编译和程序运行,一步步地完成了整个 FibServ 项目的开发过程,并为你详细介绍了这个项目的各个重要组成部分在代码层面的具体实现方式。在“C 工程实战篇”的最后,用两讲的篇幅带你实现了一个完整的 C 语言项目。希望通过这些内容,你能够对 C 语言在真实项目中的应用方式有更深刻的理解。同时,也希望能以此为起点,在实践中持续运用这个模块中介绍到的各种 C 标准库功能与工程化实践技巧,进一步加深对整个 C 语言,乃至相关技术、工具和框架的理解。原创 2023-10-03 17:45:11 · 113 阅读 · 0 评论 -
23|实战项目(上):一个简单的高性能 HTTP Server
今天主要介绍了与本次实战项目相关的一些理论性知识,以便为下一讲的实际编码打下基础。我们要构建的是一个名为 FibServ 的程序,该程序在运行时会扮演 HTTP 服务器的角色,并持续监听来自本地的 HTTP 请求。相应的请求需要为 GET 方法,并携带名为 “num” 的查询参数。FibServ 在收到该类型请求后,会计算斐波那契数列中在对应位置上的项,并将该值返回给客户端。原创 2023-10-03 17:18:55 · 155 阅读 · 0 评论 -
22|生产加速:如何使用结构化编译加速 C 项目构建?
今天主要介绍了如何组织 C 项目的源代码目录结构,以及如何在(类)Unix 系统上使用 Makefile 和 CMake 等工具,来进行 C 项目的结构化编译与跨平台自动化构建。其中,对于如何组织 C 项目的源代码目录结构,社区并没有所谓的最佳实践。正确的方式是结合项目的实际情况,在保证清晰易懂的前提下,再对项目结构进行及时、动态的调整。相较于每次使用完整的编译命令,结构化编译可以通过复用各类中间编译结果,进一步提升编译效率。同时,“可编程”的编译配置脚本也使得项目的编译细节更具可读性与可用性。原创 2023-10-03 16:41:43 · 83 阅读 · 0 评论 -
20|生产加速:C 项目需要考虑的编码规范有哪些?
这一讲,以 GNU 编码规范为例,介绍了在进行 C 项目编码时需要注意的几方面内容。从最基本的编码格式到可能影响程序正确运行的可移植性,这些内容涵盖了一个完整 C 工程项目在制定编码标准时需要考虑的绝大部分问题。GNU 编码规范的出发点,是确保所有 GNU 旗下项目都能够采用统一的编码行为,以保证这些软件都能够在 GNU 操作系统上正确无误地运行。了解 GNU 规范的基本内容之后,你就能以此为基础,根据自身需求制定更加详细和个性化的 C 编码规范。原创 2023-10-03 15:23:43 · 71 阅读 · 0 评论 -
19|极致优化(下):如何实现高性能的 C 程序?
今天主要介绍了四种可用于实现高性能 C 程序的技巧:循环展开让我们可以进一步利用 CPU 的指令级并行能力,让循环体执行得更快;优先使用条件传送指令,让我们可以在一些特定的场景中,防止使用条件分支指令带来的 CPU 周期浪费;使用更高的编译优化等级,可以借编译器之手,利用更多“黑科技”进一步优化的代码;尾递归调用优化让我们可以用循环代替递归,减少函数调用时的栈帧创建与销毁过程,让递归进行得更快。原创 2023-10-03 03:52:19 · 302 阅读 · 0 评论 -
21|生产加速:如何使用自动化测试确保 C 项目质量?
通常来说,我们谈到的“测试自动化”是指使用脚本或测试框架,以编程的方式代替传统的人工方式,来对软件功能进行测试的过程。因此,只要涉及了测试框架、软件或脚本的使用,就可以称这样的测试为自动化测试。但与之相比更有意义的,是如何进一步地把一些重要的测试环节融入到的日常开发和功能迭代中,来让整个 DevOps(即软件开发、质量保证以及系统运维)的过程更加自动化。通常的做法是,让一些针对软件基础功能的测试(如单元测试、功能测试等)成为每一次生产发布前都必须执行的环节,并指定一定的覆盖率作为通过基准。原创 2023-10-03 15:39:37 · 108 阅读 · 0 评论 -
17|标准库:断言、错误处理与对齐
最后,再来看看有关对齐的内容。关于对齐的一些理论性知识,曾在 07 讲 中介绍过。这里,来看看如何在 C 语言中为数据指定自定义的对齐方式。默认情况下,编译器会采用自然对齐,来约束数据在内存中的起始位置。但实际上,我们也可以使用 C11 提供的关键字 _Alignas ,来根据自身需求为数据指定特殊的对齐要求。并且,头文件 stdalign.h 还为我们提供了与其对应的宏 alignas,可以简化关键字的使用过程。原创 2023-10-03 03:11:48 · 141 阅读 · 0 评论 -
18|极致优化(上):如何实现高性能的 C 程序?
这一讲,主要介绍了可用于 C 代码优化的几种常见策略,它们分别是利用高速缓存、使用代码内联和 restrict 关键字,以及消除不必要的内存引用,具体如下:高速缓存利用了 CPU 的局部性,使得满足局部性的程序可以更加高效地访问数据;代码内联通过直接使用函数定义替换函数调用的方式,减少了程序调用 call 指令带来的额外开销;restrict 关键字通过限制指针的使用,避免 aliasing,进而给予了编译器更多的优化空间;原创 2023-10-03 03:31:33 · 607 阅读 · 0 评论 -
16|标准库:日期、时间与实用函数
这一讲,主要介绍了 C 标准库中与时间(日期)处理、字符串和数值转换、随机数生成、动态内存管理、进程控制,以及搜索排序等功能相关的接口。其中,第一部分功能的接口由 time.h 提供,其余部分由 stdlib.h 提供。C 语言中的时间可以被分为日历时间与处理器时间。通过名为 time 与 clock 的两个接口,我们可以分别获得与它们对应的两个值。进一步,借助 localtime 与 strftime 等接口,日历时间可以被转换为本地时间,并按照指定的形式进行格式化。原创 2023-10-03 02:33:00 · 188 阅读 · 0 评论 -
15|标准库:信号与操作系统软中断有什么关系?
从广义上来讲,信号实际上是操作系统提供的一种可以用来传递特定消息的机制。通过这种方式,操作系统可以将程序运行过程中发生的各类特殊情况转发给程序,并按照其指定的逻辑进行处理。每一种具体的信号都有它对应的名称,这些名称以 “SIG” 作为前缀。比如,当程序访问非法内存时,便会产生名为 SIGSEGV 的信号。而除零异常则对应着名为 SIGFPE 的信号。信号的产生是一个随机的过程。毕竟,异常的出现并不是人们预期中会发生的事情。原创 2023-10-02 18:25:49 · 99 阅读 · 0 评论 -
12|标准库:非本地跳转与可变参数是怎样实现的?
在这个简单的实验中,我们将构建自己的 setjmp 与 longjmp 函数,并重新编译之前的 C 示例程序,来让它使用这两个函数的自定义实现。这里,我们将直接编写汇编代码,并在最后以对象文件的形式将它们链接到程序中使用。需要注意的是,下面将要介绍的实现方式仅适用于 x86-64 平台。对于其他平台,使用的汇编指令以及需要保存的寄存器可能有所区别,但整体思路基本一致。setjmp:ret其中,前两行以 “.” 开头的语句为汇编器指令,它们用来指示汇编器应该如何处理接下来的汇编代码。原创 2023-10-02 15:25:25 · 112 阅读 · 0 评论 -
13|标准库:你需要了解的 C 并发编程基础知识有哪些?
这一讲,主要介绍了与 C 并发编程有关的一些基本概念。当然,这些概念本身都较为通用,它们也可以应用在其他编程语言的环境中。首先对比了进程与线程两者之间的不同。其中,进程主要划分了运行程序所享有的资源边界;而线程则在共享进程资源的情况下,独立负责不同子任务的执行流程。通过使用多线程,程序可以利用多核 CPU 的计算资源,做到真正的任务并行。接下来,介绍了如何在 C 代码中创建线程。C 标准将与线程控制相关的接口整合在了名为 threads.h 的头文件中。原创 2023-10-02 17:35:56 · 160 阅读 · 0 评论 -
11|标准库:深入理解标准 IO
今天主要介绍了 C 标准库中与标准 IO 相关的内容,包括 IO 接口的不同级别,它们之间的区别,以及背后的实现方式。根据对操作系统依赖关系的强弱,IO 接口可以被分为“低级 IO”与“标准 IO”两种不同的层级。其中,低级 IO 的使用依赖于具体的操作系统,而标准 IO 则抽象出了通用的 IO 接口,因此更具可移植性。标准 IO 一般会使用所在平台的低级 IO 接口来实现。而低级 IO 则通过调用操作系统内核提供的系统调用函数,来完成相应的 IO 操作。原创 2023-10-02 14:12:25 · 71 阅读 · 0 评论 -
14|标准库:如何使用互斥量等技术协调线程运行?
在本讲中,主要介绍了有关互斥量、原子操作、条件变量,以及线程本地变量的相关内容。合理地使用这些方式,我们就可以避免多线程应用经常会遇到的,由于数据竞争、竞态条件,以及指令重排引起的问题。其中,互斥量让我们可以通过对它进行加锁与解锁的方式,来限制多个线程的执行,以让它们有序地使用共享资源。在 C 语言中,互斥量被分为三种类型,mtx_plain 为最基本类型,mtx_recursive 可以被用在需要重复加锁的场景中,而 mtx_timed 则使得互斥量具有了超时属性。原创 2023-10-02 17:51:18 · 144 阅读 · 0 评论 -
10|标准库:字符、字符串处理与数学计算
今天主要介绍了 C 语言标准库中与字符、字符串处理,以及数学运算有关的内容。首先,介绍了 C 语言中字符和字符串类型变量的定义方式。字符以单引号形式定义,而字符串以双引号形式定义。字符串的不同定义方式还可能对程序的运行细节带来影响。其中,以字符数组形式定义的字符串包含有原始字符串在 .rodata 中的拷贝;而以指针形式定义的字符串变量则直接引用了 .rodata 中的字符串数据,且其值通常无法在程序运行时被动态修改。接下来,快速介绍了 C 标准库中与字符、字符串处理相关的一些函数的使用方式。原创 2023-10-02 13:43:53 · 122 阅读 · 0 评论 -
08|操控资源:指针是如何灵活使用内存的?
这一讲,主要介绍了 C 语言中有关指针的一些话题,包括指针在 C 语言中的基本使用方式、指针与数组的关系、指针的算数与关系运算,以及它们在机器指令层面的实现细节。同时,我还介绍了堆内存指针,并和你简单探讨了在使用 C 指针时需要注意的一些问题。在 C 代码中,通过添加特定的 “ * ” 符号,我们可以声明所定义变量为一个指针类型。而与指针有关的两个常用操作符为取地址操作符 “&” 与解引用操作符 “ * ”,它们一般可以通过 lea 指令与 mov 指令来实现。指针与数组也有着密不可分的联系。原创 2023-10-02 13:42:14 · 165 阅读 · 0 评论 -
09|编译准备:预处理器是怎样处理程序代码的?
预处理器在进行宏展开和宏替换时,只会对源代码进行简单的文本替换。在某些情况下,这可能会导致宏函数所表达的计算逻辑与替换后 C 代码的实际计算逻辑产生很大差异。因此,在编写宏函数时,我们要特别注意函数展开后的逻辑是否正确,避免由 C 运算符优先级等因素导致的一系列问题。接下来,就让我们一起看下:当在 C 代码中使用预处理器时,有哪些 tips 可以帮助我们避免这些问题。到这里,对于定义简单宏函数时可能遇到的一系列问题,相信你都能处理了。但当宏函数逐渐变得复杂,函数体内不再只有一条语句时,新的问题又出现了。原创 2023-10-02 12:22:19 · 148 阅读 · 0 评论 -
07|整合数据:枚举、结构与联合是如何实现的?
这一讲主要围绕着 C 语言中的枚举、结构与联合这三种数据类型展开了介绍,一起探究了它们在机器指令层面的具体实现方式。枚举这种数据类型,用于表示可取值范围有限的抽象实体。枚举类型中的枚举值又被称为“具名整型”,因此在 C 代码中,它可以直接被当作整数值来使用。同样地,在编译器生成的代码中,枚举值将被直接替换为对应的整数值。但需要注意的是,要在进行 C 编码时保证枚举值和它对应的整数值不被乱用。结构是一种用于组织异构数据的复合数据类型。在结构中,所有定义的数据字段在内存中按顺序排列。原创 2023-10-02 01:47:28 · 84 阅读 · 0 评论 -
06|代码封装(下):函数是如何被调用的?
这一讲,主要讨论了有关 C 函数的另外三个话题,分别是函数参数求值顺序、尾递归调用优化,以及 K&R 函数声明。首先,编译器对函数参数的求值顺序并不固定,因此,不要试图编写需要依赖于特定参数求值顺序才能正常运行的代码逻辑。其次,对递归函数的不正确使用,可能会导致进程栈内存出现溢出。而通过尾递归优化,编译器可以将函数的递归调用实现由 call 指令转换为条件跳转指令,从而大大减少函数调用栈帧的产生,进而避免了栈溢出的问题。不仅如此,这种方式也在一定程度上提高了函数的执行性能。原创 2023-10-02 01:06:21 · 99 阅读 · 0 评论 -
05|代码封装(上):函数是如何被调用的?
这一讲,首先快速回顾了 C 语言中函数的具体用法,然后介绍了编译器在实现 C 函数调用时需要关注的一系列规则,即 C 函数中的调用约定。在类 Unix 系统上,编译器通常会使用名为 System V AMD64 ABI 的调用约定,来作为实现函数调用的事实标准。SysV 调用约定中规定了函数在调用时需要注意的参数传递、寄存器使用,以及堆栈清理等方面的具体规则。每一个被调用函数都会在栈内存中存放与其对应的栈帧结构。原创 2023-10-02 00:34:12 · 163 阅读 · 0 评论 -
04|控制逻辑:表达式和语句是如何协调程序运行的?
今天主要讲解了 C 语言中用于描述程序运行逻辑的两种“控制单元”,即表达式和语句背后的实现细节。表达式作为一种表达计算的基本语法结构,对它的求值过程需要根据参与运算符的结合性与优先级,按一定顺序进行。而计算的具体顺序则会在语法分析阶段,由编译器直接体现在对应的 AST 结构形态上。语句是程序的基本构建块,通过不同种类语句的组合使用,可以控制程序的执行逻辑。C 语言中的语句主要包括复合语句、表达式语句、选择语句、迭代语句,以及跳转语句共五种。原创 2023-10-01 18:40:07 · 224 阅读 · 0 评论 -
03|计算单元:运算符是如何工作的?
今天主要围绕 C 语言中的基本“计算单元”,运算符,讲解了 C 语言中的几类不同运算符是如何被编译器实现的。具体总结如下:通常来说,算数、关系、位、赋值运算符的实现在大多数情况下,都会直接一一对应到特定的汇编指令上;逻辑运算符的实现方式则有些不同,它会首先借助 test 、 cmp 等指令,来判断操作数的状态,并在此基础上再进行相应的数值转换过程;在成员访问运算符中,取地址运算符一般对应于汇编指令 lea ,解引用运算符则可直接使用 mov 指令来实现;原创 2023-10-01 15:32:37 · 139 阅读 · 0 评论 -
02|程序基石:数据与量值是如何被组织的?
来总结一下。这一讲,主要介绍了 C 语言中与量值和数据相关的基本语法形式、数据实际存储时的具体编码方式,以及数据在程序运行过程中的实际存储位置等相关知识。在 C 语言中,我们可以通过多种语法形式来控制一个变量的属性,比如变量的类型、生存期、值存储位置等。数值类型变量所具有的符号性为我们进一步精细化程序逻辑提供了可能。在计算机内部,数据以二进制比特位的形式进行存放和使用。原创 2023-10-01 00:14:09 · 91 阅读 · 0 评论 -
01|快速回顾:一个 C 程序的完整生命周期
回顾了一个 C 程序的完整生命周期:代码编写、编译、运行。其中,C 代码的完整编译过程可以分为代码预处理、编译优化、汇编、链接四个阶段。程序的汇编、链接与运行,都会涉及与所在操作系统相关的一系列精细处理过程。除此之外,还从语言本身的角度,探讨了 C 语言与其他编程语言的不同之处。C 语言作为一种命令式编程语言,抽象程度更低,语法结构可以直接由相应的机器指令经过简单的组合来实现。原创 2023-09-30 17:08:27 · 144 阅读 · 0 评论