关于GNUC的语法约束学习

介绍

我们必须明白一件事情,如果你是从大学开始学C语言的话,你不可能学的非常精通,比如我们学校就是如此,所以在毕业之后的简历上也千万不要写自己会C语言,因为难度会很大,他可能会面试你一些关于GCC底层的逻辑包括GUNC的执行,代码编译和解释的过程等等,这些问题的难度很大,当然,这就是本博文的主要对象,那就是GCC处理一种比C99更牛的准则,GUNC

因为GUNC所有内容都是前后双下划线,为了避免与markdown语法重叠而加粗影响理解,这里只画前面的

__attribute

__attribute是一个GNUC对齐的最基本属性,此属性包含很多校验方案,比如函数是否被使用,或者对齐模式,参数校验等等。

aligned(“size”)

首先默认情况下gcc会对结构体进行对齐,如果一个结构体不进行对齐,那么他所占的空间就是所有属性内存空间的和,所以也就意味着如果内部包含一个可变长度的数据类型,那么就必然会发生内存溢出。这样是不安全的,需要对内部元素进行转移扩容,也需要实时判断。

如果使用内存对齐,那么在分配内存空间时就会按照结构体内空间最大的属性进行对齐,我们需要传递一个内存单元的大小,然后依次放入元素,如果元素可以正好放入n个单元内,则结构体的大小是n*一个单元的大小,注意这里内部属性的内存空间并没有改变。如果分配的单元小于最大的,那么更改为最大的单元进行对齐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

struct Aim{
int a;
char b;
short c;
} __attribute__((aligned(4))) aa;

int main() {
printf("int=%llu\n",sizeof(int));
printf("char=%llu\n",sizeof(aa.b));
printf("short=%llu\n",sizeof(aa.c));
printf("struct=%llu\n",sizeof(aa));
}
// ------------------
int=4
char=1
short=2
struct=8

1

其中的char和short因为不足4字节,所以共用一块内存单元。最后空余一个1字节的大小

如果我们没有指定内存单元的大小,GCC会使用结构体内某个内存空间最大的属性当作2的指数,然后进行分配。当然这个数字随着内部最大对象的增加而增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
struct Aim{
int a;
short b;
short c;
short d;
short e;
short f;
short g;
} __attribute__((aligned())) aa;

int main() {
printf("int=%llu\n",sizeof(int));
printf("struct=%llu\n",sizeof(aa));
}
// -----------------
int=4
struct=16

原因是struct最大的对象是int,占4字节,所以最后大小为2^4=16,如果有两个int就会变成32,如果增加属性使他超过内存大小,则进行翻2倍。

当然,这种对齐方式与你结构体内属性的先后顺序有关系,我们对他稍作修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

struct Aim{
char b;
int a;
short c;
} __attribute__((aligned(4))) aa;

int main() {
printf("int=%llu\n",sizeof(int));
printf("char=%llu\n",sizeof(aa.b));
printf("short=%llu\n",sizeof(aa.c));
printf("struct=%llu\n",sizeof(aa));
}
// -----------------
int=4
char=1
short=2
struct=12

我们看到仅仅交换了char和int的位置,内存空间就已经变化了,因为int类型已经占满了一个4字节的区域,不可能和char共用一个区域了,所以三者依次占4字节。

2

packed

这个功能是取消内存对齐,如果是追求高效率的话就必须做好内存管理,非常容易溢出。不进行对齐后内存大小就是所有属性的和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

struct Aim{
char b;
int a;
short c;
} __attribute__((packed)) aa;

int main() {
printf("int=%llu\n",sizeof(int));
printf("char=%llu\n",sizeof(aa.b));
printf("short=%llu\n",sizeof(aa.c));
printf("struct=%llu\n",sizeof(aa));
}
// ---------------------
int=4
char=1
short=2
struct=7

使用这种方法对齐和使用1字节对齐是一样的,但是因为指派1字节会导致int类型无法放入,在后面会解释为什么packed是1字节对齐。

constructor&destructor

使用这两个属性声明的函数,可以在main函数执行前后执行,可以脱离main的摆弄。使用constructor可以在main函数之前执行,使用destructor可以在main函数之后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

__attribute__((destructor())) void onDisable(){
printf("onDisable\n");
}
__attribute__((constructor())) void onLoad(){
printf("onLoad\n");
}

int main() {
printf("main\n");
}
// ----------------------------------
onLoad
main
onDisable

如果有很多这样的需要提前执行的函数,就需要在参数的括号内加入优先级吗,提前执行的对象,数字越小越先执行,拖后执行的对象,数字越大越先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

__attribute__((destructor(1))) void onDisable1(){
printf("onDisable1\n");
}
__attribute__((constructor(1))) void onLoad1(){
printf("onLoad1\n");
}
__attribute__((destructor(2))) void onDisable2(){
printf("onDisable2\n");
}
__attribute__((constructor(2))) void onLoad2(){
printf("onLoad2\n");
}

int main() {
printf("main\n");
}
// ---------------------------------
onLoad1
onLoad2
main
onDisable2
onDisable1

__alignof

这是与__attribute相反的关键字,前者是使用某种方法对齐,这个是获取对齐的字节数,可以作为aligned()的参数,我们这时就可以证明一下上文所述对齐的具体内容了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
struct A{
int a;
char b;
short c;
} __attribute__((aligned(4))) a;

int main() {
printf(" __alignof__(int)=%llu\n", __alignof__(int));
printf(" __alignof__(char)=%llu\n", __alignof__(char));
printf(" __alignof__(short)=%llu\n", __alignof__(short));
printf(" __alignof__(A)=%llu\n", __alignof__(a));
}
// ----------------
__alignof__(int)=4
__alignof__(char)=1
__alignof__(short)=2
__alignof__(A)=4

我们可以看到A的对齐方法是4个字节,和我们设置的一样,如果我们不加入参数,对齐的字节数就是最大内存字节数的以2为底的指数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
struct A{
int a;
char b;
short c;
} __attribute__((aligned())) a;

int main() {
printf(" __alignof__(int)=%llu\n", __alignof__(int));
printf(" __alignof__(char)=%llu\n", __alignof__(char));
printf(" __alignof__(short)=%llu\n", __alignof__(short));
printf(" __alignof__(A)=%llu\n", __alignof__(a));
}
// -------------------------
__alignof__(int)=4
__alignof__(char)=1
__alignof__(short)=2
__alignof__(A)=16

最后我们看一下不使用对齐会是什么结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
struct A{
int a;
char b;
short c;
} __attribute__((packed)) a;

int main() {
printf(" __alignof__(int)=%llu\n", __alignof__(int));
printf(" __alignof__(char)=%llu\n", __alignof__(char));
printf(" __alignof__(short)=%llu\n", __alignof__(short));
printf(" __alignof__(A)=%llu\n", __alignof__(a));
}
// ------------------
__alignof__(int)=4
__alignof__(char)=1
__alignof__(short)=2
__alignof__(A)=1

我们看到所谓不使用对齐就是按1个字节进行对齐,这样我们非常清晰明了的知道了__attribute的对齐逻辑。

__asm

这种方法可以在C语言执行时插入汇编指令,比如声明内存进行了改动,让gcc进行注意

1
2
3
4
5
6
#include <stdio.h>

int main() {
__asm__ ("":::"memory");
int a = 1;
}
1
2
3
4
5
6
7
8
9
10
main:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
call __main
movl $1, -4(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
ret

当然我可以使用关键字volatile进行声明内存易变。

1
2
3
4
5
#include <stdio.h>

int main() {
volatile int a = 1;
}
1
2
3
4
5
6
7
8
9
10
main:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
call __main
movl $1, -4(%rbp)
movl $0, %eax
addq $48, %rsp
popq %rbp
ret

我们从汇编上可以看到两者完全相同,没错volatile的功能和使用__asm__("":::"memory")一样,不过__asm可以插入更丰富的汇编指令,这里不做过多演示

__extension

此关键字后面的表达式,如果出先非标准的警告,不过一般人无法刻意写出bug,这里我就不举例了。

__cplusplus

这个关键字就是__cplusplus不是双下划线,这个关键字是为了方便CPP和C之间互通的,在预编译器内可以判断这是否为一个C++文件。这里我新建一个C文件,然后写入一下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.c
#include <stdio.h>
void show() {
printf("Hello, C!");
}
// 如果没有定义__cplusplus证明这是C文件,否则就是C++文件
#ifdef __cplusplus
void show() {
printf("Hello, C++!");
}
#endif

int main() {
show();
}

使用这种方法可以让C和C++使用同名函数,提高代码的复用性

其他写法

零长数组:注意,内存大小是0哦

1
int s[0];

case:可以提供一个范围,下面表示0到9

1
case 0 ... 9:

语句表达式:({各种执行的操作………;}),在所有操作的最后就是返回的值。

1
2
3
4
5
6
7
#include <stdio.h>

#define min_t(typename,x,y) ({ typename __x = (x); typename __y = (y); __x < __y ? __x: __y; })

int main() {
printf("%d",min_t(int,2,3));
}

typeof: 把一个对象的类型定义给另一个对象

1
2
int x = 2;
typeof(x) _x = 3;

可变参数:同折叠表达式,使用“…”表示一个粘连表达式,也就是##对象

当然还有很多的函数,比如可以获取当前函数名的__FUNCTION等等

总结

本文着重描写了内存对齐的具体过程以及如何进行优化,了解到内存的分配机制,可以帮助基础程序员变成大佬程序员,无论是在C的结构体的内存分配,还是针对不同架构相同数据类型的数据对齐都是使用的这种方法,所以C语言是一个可以以小见大的好语言。


关于GNUC的语法约束学习
https://blog.minloha.cn/posts/201644ccaac5ce2022121633.html
作者
Minloha
发布于
2022年12月16日
更新于
2024年9月15日
许可协议