一文了解GCC(GNU C)语法
  xfEU0RRJpeyr 2023年11月02日 24 0


要研究LINUX内核,C语言是基础中的基础,但是LINUX并不是完全的标准C,而是对标准C做了很多扩展,这些扩展特性对于我们分析内核有着很重要的作用,下面做些总结性的工作。

1 语句表达式

({
int y=foo();
int z;
if(y>0)
z=y;
else
z=-y;
z;
})

这种特性在宏定义中尤为安全。(因为他们对操作数只进行一次赋值)。这里定义了一个安全的求最小值的宏,在标准C中,通常定义为:

#define max(a,b) ((a)>(b)?(a):(b))

在这种定义下,如果他们有副作用(如果a或b是自增或自建变量的话)的话,a和b将进行两次运算,会得到错误的结果。而且如果两个变量的类型不一致,同样也会导致错误。

在GNU C中,如果你知道操作数的类型(这里假定为整型),你可以这样来定义这个宏:

#define maxint(a,b) ({int __a=(a); int __b=(b); __a > __b ? __a : __b; })

或者将参数类型作为宏的一个参数传入进去:

/* 两个参数类型相同时 */
#define max__t(type,a,b) ({\
type __a=(a);\
type __b=(b);\
__a > __b ? __a : __b; })

当然,如果你不知道参数的具体类型,你也可以使用typeof或____auto__type运算符。

/* 两个参数类型不同时,会发出警告 */
#define max(a,b) ({\
const typeof(a) __a = (a);\
const typeof(b) __b = (b);\
(void)(&__a == &__b);\ //检查__a 和__b 的类型是否一致
__a > __b ? __a : __b; })

2 局部标签

  • 为什么要使用局部标签:在复杂的宏定义中,如果一个宏包含有内嵌循环,goto语句可以方便地跳出它们。然而,拥有整个函数作用域的普通标签在这里不能被使用,因为该宏可能会在一个函数中被展开若干次,那样的话同样的一个标签就会被重复定义。局部标签就是用来避免这种情况的
  • 怎么使用局部标签:GCC允许在任何内嵌代码块中声明局部标签,所谓的局部标签跟普通的标签用法一样(用在goto语句或者被获取地址),只不过你只能在声明它的代码块中使用
  • 局部标签的声明
__label__  label; 
或者
__label__ label1, label2, ...;

局部标签声明只是定义了标签的名字,但是并没有定义标签本身,它本身必须像普通标签那样在语句内嵌表达式内部使用局部标签。

另外要注意的是,局部标签的声明必须在代码块的**起始位置**(即位于任何其他声明和语句之前)。

  • 举例说明
#define SEARCH(value, array, target)\
do {\
__label__ found;\ //声明局部标签“found”
typeof(target) _SEARCH_target = (target);\
typeof(*(array)) *_SEARCH_array = (array);\
int i, j, value;\
for (i = 0; i < max; i++)\
for (j = 0; j < max; j++)\
if (_SEARCH_array[j] == _SEARCH_target){\
(value) = i;\
goto found;\
}\
(value) = -1;\
found:;\ //跳出循环
}while (0)

当然,也可以用语句表达式改写这个宏定义:

#define SEARCH(value, array, target)\
({ __label__ found;\
typeof (target) _SEARCH_target = (target);\
typeof (*(array)) *_SEARCH_array = (array);\
int i, j, int value;\
for (i = 0; i < max; i++)\
for (j = 0; j < max; j++)\
if (_SEARCH_array[j] == _SEARCH_target){\
(value) = i;\
goto found; }\
(value) = -1;\
found:value;\ //语句表达式最后的返回值是value
})
  • 注意:例子代码未必准确,仅展示局部标签的用法。我们用语句表达式的时候,在最后的found局部标签后面有个语句 value; 而在do … while循环中并无出现,原因是语句表达式的值取决于最后的表达式,而do … while 循环中的found局部标签仅仅用来跳出循环。

3 变参宏

标准C的变参宏

  • 在ISO C99里,一个宏可以被声明为带可变的参数个数,就像函数一样。语法如下:
#define  debug(format,...)  fprintf(stderr, format, __VA_ARGS__)
  • 这里的 “…” 代表变参,在引用宏debug的地方它代表着零个或多个相应的标识符,包括逗号。这些标识符将会替换​​__VA_ARGS__​​。
  • 但是这样的宏不能处理零变参的情况,否则编译不会通过,因为零变参的时候会多一个逗号

GCC的变参宏

  • GCC 支持变参宏,并且提供另一种词法来定义它,即可赋予变参名称,就像普通参数一样:
#define  debug(format, args...)  fprintf(stderr, format, args)
  • 这种用法与上面所述的ISO C形式的宏定义完全一样,只是看起来更具阅读性。
  • args跟后面的三个点可以连在一起,也可以用空格分开,当然这个宏同样不支持零变参的情形,原因同上。

GNU C的变参宏

  • 除了前面提到的可以为变参命名之外,GNU 预处理器CPP对ISO C的变参宏还进行了进一步的扩展,使之能处理零变参个数的情况。
  • 举例来说,以下这个语句在ISO C编译器中编译时是错的:​​debug(“A message”)​​。在ISO C中不允许省略所有的变参,因为在这个字符串之后多了一个逗号”,”。
  • GNU预处理器CPP允许你省略全部的变参,方法是在变参前加上黏贴符“##”
#define debug(format,...) fprintf(stderr, format,  ##__VA_ARGS__)

或者

#define  debug(format, args...)  fprintf(stderr, format, ##args)

这样,当我们省略变参的时候黏贴符能自动清除前面多余的逗号

  • 另外,在宏里面,除了两个井号 ## 可以作为黏贴符之外,其实一个井号 # 也可以用来黏贴符号,但是它要被用在字符串当中,例如:
#define prt(n) printf("calculate i'n: i#n  = %d, with parameter %d\n", i##n, n) 

int i = 1;
int i8 = 800, i9 = 900;
prt(8);
prt(9);

执行的结果如下:

calculate i'n: i8 = 800, with parameter 8
calculate i'n: i9 = 900, with parameter 9
  • 在字符串中,我们可以用一个井号来黏贴宏参数,就像上面我们看到的那样。其中字符串与黏贴字符之间的空格是可选的,预处理器会自动去掉多余的空格。

**总结:**在上面的例子中:

#define prt(n) printf("calculate i'n: "  "i#n = %d\n", i##n, n)
  • 故意在四个地方都用到了标识符n(那个转义换行符’\n’不在讨论范围内),依次分析是:
  1. 在字符串中直接出现的“宏参数”实际上并不会被当成参数,而是一个普通的字符n;
  2. 如果要解决第一个问题,那就要在字符串当中使用一个井号 # 来黏贴宏参数;
  3. 不在字符串当中要黏贴宏参数,则需要两个井号 ## 来黏贴;
  4. 不在字符串中,如果直接出现宏参数,则预处理器将进行宏展开。

4 case范围

在GCC中,你可以在case标记后面指定一个连续值,例如:

case low...high:

这种写法等价于把每个值独立成一个个case标记的情况:

case low:
case low+1:
...
case high:

这个特性对于要写连续的ASCII码值的时候特别有用:

switch (ch)
{
case '0' ... '9':
c -= '0';
break;
case 'a' ... 'f':
c -= 'a' - 10;
break;
case 'A' ... 'F':
c -= 'A' - 10;
break;
}

注意:在“…”的左右两边一定要有空格,否则编将有词法错误。

5. 标号元素

  • 在标准C里,数组或者结构变量的初始化值必须以固定的顺序出现,而在GCC(GNU C)中,通过制定索引或者结构域名,则允许初始化值以任意顺序出现。
  • 指定数组索引的方法是在初始化值前面写​​[INDEX]=​​​,还可以使用​​[FIRST … LAST]=​​的形式指定一个范围。比如:
int  array[20] = {[2] = 100, [10 ... 19] = 200};

对于结构体初始化:

struct file_operations ext2_file_operations =
{
llseek: generic_file_llseek,
read: generic_file_read,
write: generic_file_write,
ioctl: ext2_ioctl,
mmap: generic_file_mmap,
open: generic_file_open,
release: ext2_release_file,
fsync: ext2_sync_file,
};
/* 但在linux中,更倾向于使用如下方式*/
struct file_operations ext2_file_operations =
{
.llseek = generic_file_llseek,
.read = generic_file_read,
.write = generic_file_write,
.aio_read = generic_file_aio_read,
.aio_write = generic_file_aio_write,
...
.writev = generic_file_writev,
.sendfile = generic_file_sendfile,
};
  • 使用这种形式,当结构体的定义变化导致元素的偏移位置改变时,仍然可以确保已知元素的正确性。对于未出现在初始化中的元素,其初值为0。

6. 零长度数组

GNU C 允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用。例如:

struct var_data
{
int len;
char data[0];
};

char data[0]仅仅意味着程序中通过 var_data 结构体实例的 data[index]成员可以访问 len 之后的第 index 个地址,它并没有为 data[]数组分配内存,因此 sizeof(struct var_data) = sizeof(int)。

假设 struct var_data 的数据域保存在 struct var_data 紧接着的内存区域,通过如下代码可以遍历这些数据:

struct var_data s;
...
for (i = 0; i < s.len; i++)
{
printf("%02x", s.data[i]);
}

7. 函数名

  • GNU C中预定义两个标志符保存当前函数的名字,​​__FUNCTION__​​​保存函数在源码中的名字,​​__PRETTY__FUNCTION__​​保存带语言特色的名字。
  • 在C函数中这两个名字是相同的。在C++函数中,​​__PRETTY_FUNCTION__​​​包括函数返回类型等额外信息,Linux内核只使用了​​__FUNCTION__​​。
//fs/ext2/super.c
void ext2_update_dynamic_rev(struct super_block* sb)
{
struct ext2_super_block *es = EXT2_SB(sb)->s_es;
if(le32_to_cpu(es->s_rev_level)->EXT2_GOOD_OLD_REV)
return;
ext2_warning(sb, __FUNCTION__,\
"updating to rev %d becauseof new feature flag",\
"running e2fsck is recommended",\
EXT2_DYNAMIC_REV);}
  • 这里​​__FUNCTION__​​​将被替换为函数名​​ext2_update_dynamic_rev​​。
  • 虽然​​__FUNCTION__​​​看起来类似于标准C中的​​__FILE__​​​,但实际上**​​__FUNCTION__​​​是被编译器替换的,而 ​​__FILE__​​是被预处理器替换**。
  • 在C99中支持​​__func__​​​宏,因此建议使用​​__func__​​​替代​​__FUNCTION__​​。

8. 特殊属性声明

GNU C允许声明函数、变量和类型的特殊属性,以便进行手工的代码优化和定制代码检查的方法。

  • no return属性用于函数,表示该函数从不返回。这可以让编译器生成稍微优化的代码,最重要的是可以消除不必要的警告信息比如未初使化的变量。例如:
//include/linux/kernel.h
#define ATTRIB_NORET __attribute__((noreturn))
void do_exit(long error_code) ATTRIB_NORET;
  • format(ARCHETYPE,STRING-INDEX,FIRST-TO-CHECK)属性用于函数,表示该函数使用printf,scanf或strftime风格的参数,使用这类函数最容易犯的错误是格式串与参数不匹配,指定format属性可以让编译器根据格式串检查参数类型。例如:
//include/linux/kernel.h
asm linkage int printk(const char* fmt, ...) __attribute__((format(printf,1,2)));
  • 表示第一个参数是格式串,从第二个参数起,根据printf()函数的格式串规则检查参数。
  • unused属性用于函数和变量,表示该函数或变量可能不使用,这个属性可以避免​​编译器​​产生警告信息。
  • **__section__(“section-name”)**属性用于函数和变量,通常编译器将函数放在.text区,变量放在.data区或.bss区,使用section属性,可以让编译器将函数或变量放在指定的节中。例如:
//include/linux/init.h
#define __init __attribute__((__section__(".text.init")))
#define __exit __attribute__((unused,__section__(".text.exit")))
#define __init data__attribute__((__section__(".data.init")))
#define __exit data__attribute__((unused,__section__(".data.exit")))

#define __initsetup __attribute__((unused,__section__(".setup.init")))
#define __init_call __attribute__((unused,__section__(".initcall.init")))
#define __exit_call __attribute__((unused,__section__(".exitcall.exit")))
  • 连接器可以把相同节(section)的代码或数据安排在一起,Linux内核很喜欢使用这种技术,例如系统的初始化代码被安排在单独的一个节,在初始化结束后就可以释放这部分内存。
  • **aligned(ALIGNMENT)**属性用于变量、结构或联合类型,指定变量、结构域、结构或联合的对齐量,以字节为单位,例如:
//include/asm-i386/processor.h
struct i387_fxsave_struct{
unsigned short cwd;
...
}__attribute__((aligned(16)));
  • 表示该结构类型的变量以16字节对齐。通常编译器会选择合适的对齐量,显示指定对齐通常是由于体系限制、优化等原因。
  • packed属性用于变量和类型,用于变量或结构域时表示使用最小可能的对齐,用于枚举、结构或联合类型时表示该类型使用最小的内存。例如:
//include/asm-i386/desc.h
struct Xgt_desc_struct{
unsigned short size;
unsigned long address;
}__attribute__((packed));
  • 域address将紧接着size分配。属性packed的用途大多是定义硬件相关的结构,使元素之间没有因对齐而造成的空洞。

9. 内建函数

GNU C提供了大量的内建函数,其中很多是标准C库函数的内建版本,例如memcpy,它们与对应的C库函数功能相同,本文不讨论这类函数,其他内建函数的名字通常以__builtin开始。

  • 内建函数​​__builtin_return_address(LEVEL)​​返回当前函数或其调用者的返回地址,参数LEVEL指定调用栈的级数,如0表示当前函数的返回地址,1表示当前函数调用者的返回地址,依此类推。例如:
//kernel/sched.c
printk(KERN_ERR "schedule_timeout: wrong timeout value %lx from %p\n",\
timeout, __builtin_return_address(0));
  • 内建函数​​__builtin_constant_p(EXP)​​用于判断一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0。
//include /asm-i386/bitops.h
/*检测第 1 个参数是否为编译时常数以确定采用参数版本还是非参数版本的代码*/
#define test_bit(nr,addr)\
(__builtin_constant_p(nr)?\
constant_test_bit( (nr),(addr) ):\
variable_test_bit( (nr),(addr) ) )
  • 很多计算或操作在参数为常数时有更优化的实现,在GNUC中用上面的方法可以根据参数是否为常数,只编译常数版本或非常数版本,这样既不失通用性,又能在参数是常数时编译出最优化的代码。
  • 内建函数**​​__builtin_expect(EXP, C)​​**用于为编译器提供分支预测信息,其返回值是整数表达式EXP的值,C的值必须是编译时常数。例如:
#include <linux/compiler.h>
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)

//kernel/sched.c
if( unlikely(in_interrupt()) ){
printk("Scheduling ininterrupt\n");
BUG();
}
  • 这个内建函数的语义是EXP的预期值是C,编译器可以根据这个信息适当地重排语句块的顺序,使程序在预期的情况下有更高的执行效率。
  • 上面的例子表示处于中断上下文是很少发生的,编译器可以将​​printk();BUG();​​这段代码放在较远的位置,以保证经常执行的目标码更紧凑。
  • 若不想使用GNUC扩展,那么只需要在gcc参数后面加上​​-ansi-pedantic​​即可,使用上述参数后,所有GNC C扩展语法部分将会有编译警报。


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
xfEU0RRJpeyr