Devin's Blog

Diversity is essential to happiness

0%

Assert 如何工作

Assert 为什么能够让程序退出?

Assert

主要讨论下assert的实现方式,以glibc-2.2版本为例

一、实验代码

1
2
3
4
5
6
7
8
9
#include <assert.h>

int main (int argc, char **argv) {
assert(1 < 0);
int i = 0;
i = i;

return 0;
}

二、实验步骤

  1. 使用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函数

  2. __assert_fail的实现

    不同版本的glibc中的__assert_fail实现不同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void
    __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
#ifdef  NDEBUG

# define assert(expr) (__ASSERT_VOID_CAST (0))

/* 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.) */

# ifdef __USE_GNU
# define assert_perror(errnum) (__ASSERT_VOID_CAST (0))
# endif

#else /* Not NDEBUG. */
....
#endif


对于gcc编译器而言,在编译时加上-DNDEBUG即可

五、(void) sizeof ((expr) ? 1 : 0)

这里使用的__extension__是一个GNU Libc的宏,用于指示编译器在处理代码时启用拓展特性。

__extension__包裹一段代码或者表达式,以指示编译器在处理该代码时启用拓展特性。

当使用__extension__包裹代码的时候,编译器不会报错,

这是因为__extension__提示编译器在处理代码时启用拓展特性,使得编译器能够接受非标准的语法或者特性,并且不产生警告或者错误(除非编译器开启严格模式)

1
2
3
4
5
6
__extension__ int x = 10;  // 声明一个使用扩展特性的变量

__extension__ ({
// 使用扩展特性的代码块
// ...
});

由于实际的断言逻辑包裹在__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