Assert 为什么能够让程序退出?
Assert
主要讨论下assert的实现方式,以glibc-2.2版本为例
一、实验代码
1
2
3
4
5
6
7
8
9
int main (int argc, char **argv) {
assert(1 < 0);
int i = 0;
i = i;
return 0;
}
二、实验步骤
使用
gcc -E main.c
查看 预处理文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c"
# 1 "/usr/include/assert.h" 1 3 4
# 35 "/usr/include/assert.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 424 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 442 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 443 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
# 444 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 425 "/usr/include/features.h" 2 3 4
# 448 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
# 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
# 449 "/usr/include/features.h" 2 3 4
# 36 "/usr/include/assert.h" 2 3 4
# 66 "/usr/include/assert.h" 3 4
# 69 "/usr/include/assert.h" 3 4
extern void __assert_fail (const char *__assertion, const char *__file,
unsigned int __line, const char *__function)
__attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__noreturn__));
extern void __assert_perror_fail (int __errnum, const char *__file,
unsigned int __line, const char *__function)
__attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__noreturn__));
extern void __assert (const char *__assertion, const char *__file, int __line)
__attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__noreturn__));
# 2 "main.c" 2
# 3 "main.c"
int main (int argc, char **argv) {
# 4 "main.c" 3 4
((void) sizeof ((
# 4 "main.c"
1 < 0
# 4 "main.c" 3 4
) ? 1 : 0), __extension__ ({ if (
# 4 "main.c"
1 < 0
# 4 "main.c" 3 4
) ; else __assert_fail (
# 4 "main.c"
"1 < 0"
# 4 "main.c" 3 4
, "main.c", 4, __extension__ __PRETTY_FUNCTION__); }))
# 4 "main.c"
;
int i = 0;
i = i;
}其中 assert(0)被展开成
1
2
3
4((void) sizeof ((1 < 0) ? 1 : 0), __extension__ ({
if (1 < 0) ;
else __assert_fail("0", "main.c", 4, __extension__ __PRETTY_FUNCTION__);
}))其中
((void) sizeof ((1 < 0) ? 1 : 0), __extension__ ({ ... }))
是一个逗号表达式其中包含一个sizeof 运算符和一个匿名模块
其中核心的是__assert_fail函数
__assert_fail
的实现不同版本的glibc中的__assert_fail实现不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void
__assert_fail (const char *assertion, const char *file, unsigned int line,
const char *function)
{
#ifdef FATAL_PREPARE
FATAL_PREPARE;
#endif
/* Print the message. */
(void) fprintf (stderr, _("%s%s%s:%u: %s%sAssertion `%s' failed.\n"),
__assert_program_name ? __assert_program_name : "",
__assert_program_name ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
(void) fflush (stderr);
abort ();
}最终 调用了abort() 让程序退出
四、如何关闭ASSERT
assert.h中有如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* void assert_perror (int errnum);
If NDEBUG is defined, do nothing. If not, and ERRNUM is not zero, print an
error message with the error text for ERRNUM and abort.
(This is a GNU extension.) */
....
对于gcc编译器而言,在编译时加上-DNDEBUG即可
五、(void) sizeof ((expr) ? 1 : 0)
这里使用的__extension__是一个GNU Libc的宏,用于指示编译器在处理代码时启用拓展特性。
__extension__
包裹一段代码或者表达式,以指示编译器在处理该代码时启用拓展特性。
当使用__extension__
包裹代码的时候,编译器不会报错,
这是因为__extension__
提示编译器在处理代码时启用拓展特性,使得编译器能够接受非标准的语法或者特性,并且不产生警告或者错误(除非编译器开启严格模式)
1 | __extension__ int x = 10; // 声明一个使用扩展特性的变量 |
由于实际的断言逻辑包裹在__extension__中,用户的入参也就是一个表达式,将被放在__extension__中
如果入参有任何错误,都将不会有编译时的报警和错误提示
所以需要设计一种方案,既能让编译器有机会提示报警,又不能实际执行表达式,
因为在__extension
中有一次执行,不能执行两次
所以这里使用了sizeof运算符,他能接受一个表达式,查看其类型,并找出其展位符,并且无需执行该表达式
举例来说:
如果我们有一个函数int blow_up_to_world();
那么表达式sizeof(blow_up_to_world)
会找到表达式结果的大小(本例中时int),当然这个函数并不会被执行
但是,如果编译器启用了 -pedantic
和 -ansi
,那么__extension
中该报错的代码还是会报错
接下来,用户的入参没有直接给 sizeof 计算,而是使用了三元。
这是因为如果入参是 可变长度的数组或者是函数名时,可能会产生不良影响
至于为什么使用逗号,因为开发者希望assert是一个表达式,而不是类似do while() 块或者其他东西,使用使用逗号,并且丢弃了 sizeof的结果
如果函数void testfunc();
执行assert(testfunc())
会有编译报错。
六、参考链接
https://stackoverflow.com/questions/56314110/libc6-comma-operator-in-assert-macro-definition