E阶段前面几部分的学习主要是在铺垫,从E4开始就有点上强度了,所以从此开始记录。
从C代码到二进制程序
Linux中的C语言进阶学习
在Linux中完成Learn C the hard way练习0~22, 练习24~25, 练习27~33, 你需要将示例代码拷贝到Linux中编译并运行, 结合文字RTFSC理解示例代码, 并完成相应的附加题.
虽然已经学了一段时间的C,但是不妨从头过一遍这些内容,还有相关的工具的使用。
Valgrind基本使用
可以根据文档中的步骤尝试一下手动编译:
# 1) Download it (use wget if you don't have curl)
curl -O http://valgrind.org/downloads/valgrind-3.6.1.tar.bz2 # 这个链接撤掉了,到官网重新找一个
# use md5sum to make sure it matches the one on the site
md5sum valgrind-3.6.1.tar.bz2
# 2) Unpack it.
tar -xjvf valgrind-3.6.1.tar.bz2
# cd into the newly created directory
cd valgrind-3.6.1
# 3) configure it
./configure
# 4) make it
make
# 5) install it (need root)
sudo make install
一般的软件的编译过程也是这样的,有时候会有些奇怪的报错,不过这次的还好。我们可以尝试编写一个格式化未提供参数的错误来测试一下效果。
> cc -Wall test.c -o test -g
test.c: In function ‘main’:
test.c:8:19: warning: format ‘%d’ expects a matching ‘int’ argument [-Wformat=]
8 | printf("I am %d years old.\n");
| ~^
| |
| int
test.c:5:9: warning: unused variable ‘age’ [-Wunused-variable]
5 | int age = 10;
| ^~~
test.c:9:5: warning: ‘height’ is used uninitialized [-Wuninitialized]
9 | printf("I am %d inches tall.\n", height);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.c:6:9: note: ‘height’ was declared here
6 | int height;
| ^~~~~~
[08:39:39] Ylin@Ylin /home/Ylin/programs/C
> valgrind test
==15312== Memcheck, a memory error detector
==15312== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==15312== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==15312== Command: test
==15312==
==15312==
==15312== HEAP SUMMARY:
==15312== in use at exit: 0 bytes in 0 blocks
==15312== total heap usage: 30 allocs, 30 frees, 3,993 bytes allocated
==15312==
==15312== All heap blocks were freed -- no leaks are possible
==15312==
==15312== For lists of detected and suppressed errors, rerun with: -s
==15312== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
我用的是和书上同一个程序,不知道为什么版本更高的valgrind无法检查测到书上所遇见的错误显示。我也不太好说这是一个更好的设计还是不好的设计,我认为他可能放过了一些对内存不产生影响的bug。
一个简单的对象系统
我们可以在C中构建一个面向对象的系统,来进一步了解OOP的工作原理,我们可以通过利用结构体和函数指针来实现一个简单的对象系统。
我们可以定义一个头文件来存放我们的私有变量和公有得方法
#include <stddef.h>
typedef struct{
char* name;
int totalDamage;
int (*init)(void *self);
void (*destroy)(void *self);
void (*addDamage)(void *self, int damage);
int (*getDamage)(void *self);
} DamageService;
int obj_init(void *self);
void obj_destroy(void* self);
void obj_addDamage(void* self, int damage);
int obj_getDamage(void* self);
void* obj_new(size_t size, DamageService proto, char* name);
void obj_delete(void* self);
#define NEW(T,N) (T*)obj_new(sizeof(T), (DamageService){0}, N)
#define CALL(obj,method,...) (obj)->method((obj), ##__VA_ARGS__)
这一部分可以看作一个类的初始化过程,但是这里并没有方法的具体实现。关键在于下面的两个宏:
NEW(T,N)将创建对象的过程封装了起来,这样可以避免潜在的调用错误CALL(obj,method)则是实现了一个调用宏
现在我们进一步实现obj.c的定义:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "obj.h"
#include <assert.h>
int obj_init(void *self){
return 1;
}
void obj_destroy(void *self){
DamageService* obj = self;
if(obj) free(obj->name);
free(obj);
}
void obj_addDamage(void *self, int damage){
DamageService* obj = self;
if(obj) obj->totalDamage += damage;
}
int obj_getDamage(void *self){
DamageService* obj = self;
if(obj) return obj->totalDamage;
return 0;
}
void* obj_new(size_t size, DamageService proto, char* name){
if(!proto.init) proto.init = obj_init;
if(!proto.destroy) proto.destroy = obj_destroy;
if(!proto.addDamage) proto.addDamage = obj_addDamage;
if(!proto.getDamage) proto.getDamage = obj_getDamage;
DamageService* t = malloc(size);
if(!t) return NULL;
*t = proto;
if(name){
t->name = malloc(strlen(name) + 1);
if(t->name) strcpy(t->name, name);
else{
free(t);
return NULL;
}
}else{
t->name = NULL;
}
if(!t->init(t)){
t->destroy(t);
return NULL;
}else{
return t;
}
}
void obj_delete(void* self){
DamageService* obj = self;
if(!obj) return;
if(obj->destroy) obj->destroy(obj);
}
现在我们就实现了一个可以用来统计伤害的"类",我们可以将其编译成目标文件,之后通过链接的方式再其他的项目中使用我们的这个类:
[19:14:40] Ylin@Ylin /home/Ylin/programs/C
> gcc obj.c -c
[19:14:47] Ylin@Ylin /home/Ylin/programs/C
> ls
obj.c obj.h obj.o
我门编写一个程序来使用它:
#include "obj.h"
#include <stdio.h>
int main(){
DamageService* obj = NEW(DamageService, "Player1");
CALL(obj, addDamage, 10);
CALL(obj, addDamage, 20);
printf("%s has taken %d damage.\n", obj->name, CALL(obj, getDamage));
obj_delete(obj);
return 0;
}
我们可以展开说说这两个宏:
-
NEW(T,N) (T*)obj_new(sizeof(T), (T){0}, N)T是类名,我们通过obj_new返回一个完成了初始化的T的指针,其中
(T){0}是一个小技巧,这里创建了一个临时的T结构体,且所有字段被填充为0,后续在程序中再进行初始化,N用来提供字段信息。 -
CALL(obj,method,...) (obj)->method((obj), ##__VA_ARGS__)...在宏中代表可变长度参数,配合##__VA_ARGS在后续添加变量,为前面的方法调用提供参数。
对C的面向对象的系统就暂时分析到这里吧,我感觉还是蛮有意思的,我打算后续再进一步研究一下这一部分。
强大的宏
刚刚我们利用宏实现了对面向对象系统的简单实现,现在我们可以进一步了解它的其他用法。
在C中,我们通常通过全局的errno来传递错误,或是返回错误码。为了保证错误能被正确的处理,在很多的C程序中,我们需要编写额外的程序来处理错误。受限于C的机制,我们很难像其他语言一样利用异常的机制来解决这个问题,所以我们需要调试宏来帮助我们解决这个问题:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#ifdef NDEBUG
#define debug(M, ...)
#else
#define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#endif
#define clean_errno() (errno == 0 ? "None" : strerror(errno))
#define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)
#define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)
#define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }
#define sentinel(M, ...) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }
#define check_mem(A) check((A), "Out of memory.")
#define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; }
依次介绍一下这里各个宏的作用:
-
debug("format",arg1,arg2,...)实际上是fprintf对stderr的调用,这用宏封装了起来。其中
__FILE__和__LINE__分别是当前文件的文件名称和行号的元信息,可以有效用来调试信息。 -
clean_errno通过一个三元组来获取errno的错误信息
-
log_err、log_warn、log_info用来为显示信息,这里是分级别显示,便于按环境进行使用
-
check(condition,"format",arg1,arg2,...)用来检测condition是否为真,否则会记录错误
M,并跳转到error标签 -
sentinel(M,...)这个宏用来放在程序中不该被直接执行的分支,一旦触发这个宏就会打印错误信息,并跳转到
error标签 -
check_mem(A)用来确保指针有效
-
check_debug(A,M,...)将check中的错误信息替换成
debug信息,可以通过定义NDEBUG来控制是否输出信息。
接下来我们可以尝试利用这些调试宏,来帮助我们发现错误
#include "dbg.h"
#include <stdlib.h>
#include <stdio.h>
void test_debug()
{
debug("I have Brown Hair.");
debug("I am %d years old.", 37);
}
void test_log_err()
{
log_err("I believe everything is broken.");
log_err("There are %d problems in %s.", 0, "space");
}
void test_log_warn()
{
log_warn("You can safely ignore this.");
log_warn("Maybe consider looking at: %s.", "/etc/passwd");
}
void test_log_info()
{
log_info("Well I did something mundane.");
log_info("It happened %f times today.", 1.3f);
}
int test_check(char *file_name)
{
FILE *input = NULL;
char *block = NULL;
block = malloc(100);
check_mem(block); // should work
input = fopen(file_name,"r");
check(input, "Failed to open %s.", file_name);
free(block);
fclose(input);
return 0;
error:
if(block) free(block);
if(input) fclose(input);
return -1;
}
int test_sentinel(int code)
{
char *temp = malloc(100);
check_mem(temp);
switch(code) {
case 1:
log_info("It worked.");
break;
default:
sentinel("I shouldn't run.");
}
free(temp);
return 0;
error:
if(temp) free(temp);
return -1;
}
int test_check_mem()
{
char *test = NULL;
check_mem(test);
free(test);
return 1;
error:
return -1;
}
int test_check_debug()
{
int i = 0;
check_debug(i != 0, "Oops, I was 0.");
return 0;
error:
return -1;
}
int main(int argc, char *argv[])
{
check(argc == 2, "Need an argument.");
test_debug();
test_log_err();
test_log_warn();
test_log_info();
check(test_check("ex20.c") == 0, "failed with ex20.c");
check(test_check(argv[1]) == -1, "failed with argv");
check(test_sentinel(1) == 0, "test_sentinel failed.");
check(test_sentinel(100) == -1, "test_sentinel failed.");
check(test_check_mem() == -1, "test_check_mem failed.");
check(test_check_debug() == -1, "test_check_debug failed.");
return 0;
error:
return 1;
}
运行结果如下:
[21:27:48] Ylin@Ylin /home/Ylin/programs/C [1]
> ./a.out 1
DEBUG test.c:8: I have Brown Hair.
DEBUG test.c:9: I am 37 years old.
[ERROR] (test.c:14: errno: None) I believe everything is broken.
[ERROR] (test.c:15: errno: None) There are 0 problems in space.
[WARN] (test.c:20: errno: None) You can safely ignore this.
[WARN] (test.c:21: errno: None) Maybe consider looking at: /etc/passwd.
[INFO] (test.c:26) Well I did something mundane.
[INFO] (test.c:27) It happened 1.300000 times today.
[ERROR] (test.c:39: errno: No such file or directory) Failed to open ex20.c.
[ERROR] (test.c:103: errno: None) failed with ex20.c
根据这些信息,我们可以很好的定位到程序出现问题的部分。
代码调试
我们将进一步的研究代码的调试,而不只是使用调试宏来给出错误的信息,我们可能会希望知道更准确的原因来定位我们程序中的错误。
接下来介绍一些常见的gdb指令,不过我们首先需要一个程序:
#include <unistd.h>
int main(int argc, char *argv[])
{
int i = 0;
while(i < 100) {
usleep(3000);
}
return 0;
}
我们可以使用以下的指令来和调试器进行交互:
help获取指令break file.c:(line|function)在暂停的地方设置断点,提供行号或者函数名来实现断点run args通过run来执行程序,args提供参数c/cont继续执行,直到断点s/step单步执行,会步入程序内部n/next单步执行,但是会步过程序bt/backtrace用来跟踪回溯函数的调用过程set var X = Y将变量X设置为Yprint X打印X的值
当然还可以使用TUI的界面,这样更方便我们平时的使用和调试:

可以通过在启动gdb时提供参数--tui,也可以使用以下命令来控制:
layout asm开启汇编窗口layout src源码窗口layout split分屏layout next/prev切换模式focus next/prev切换窗口之间的焦点
gdb还有一个实用的功能就是它可以附加调试运行中的程序,我们可以在和后台运行刚刚的程序,然后找到它的进程并暂停调试它(好神奇)
[19:28:36] Ylin@Ylin /home/Ylin/programs/C
> ps aux | grep a.out
Ylin 25578 0.3 0.0 2424 1024 pts/0 S 19:28 0:00 ./a.out
Ylin 25609 0.0 0.0 6524 2048 pts/0 S+ 19:28 0:00 grep --color=auto a.out[19:30:05] Ylin@Ylin /home/Ylin/programs/C
> sudo gdb -p 25578
[sudo] password for Ylin:
GNU gdb (Debian 16.3-1) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
Attaching to process 25578
Reading symbols from /home/Ylin/programs/C/a.out...
Reading symbols from /lib/x86_64-linux-gnu/libc.so.6...
Reading symbols from /usr/lib/debug/.build-id/fc/e446c9d4ad48e2b0c90cce1a11722897805281.debug...
Reading symbols from /lib64/ld-linux-x86-64.so.2...
Reading symbols from /usr/lib/debug/.build-id/43/3b9dde90d2932723cb05cc8e363ec0d8cd1807.debug...
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
__internal_syscall_cancel (a1=a1@entry=0, a2=a2@entry=0, a3=a3@entry=140736887692128, a4=a4@entry=0, a5=a5@entry=0, a6=a6@entry=0, nr=230)
at ./nptl/cancellation.c:44
warning: 44 ./nptl/cancellation.c: No such file or directory
(gdb) bt
通常我们可以通过这种用法来追踪core dumped的问题,通过bt来追踪程序错误发生的原因,当然GDB还有很多高级的用法,我们还可以用它来追踪内存,来查看内存中发生的变化。但是这里就不做详细的描述了,以后要用到的时候自然会接触到。
总结
这一部分主要是来自笨办法学C 中文版 · 笨办法学C,看了之后有点震撼吧,因为这本书虽然说是面向零基础的,但是实际上很多内容十分的新颖,和传统的C语言教学一点也不一样,短短的内容但是收获很多,可以说是完全不一样的体验。很多内容都值得仔细琢磨,之后我也会单独的好好看一看其中的几篇文章,感觉收获很大,看到了不一样的C语言。