欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【嵌入式】C语言高级编程-地址对齐(07)

程序员文章站 2022-07-02 08:37:22
...

00. 目录

01. 数据对齐概述

一般情况下,当我们定义一个变量,编译器会按照默认的地址对齐方式,来给该变量分配一个存储空间地址。如果该变量是一个 int 型数据,那么编译器就会按4字节或4字节的整数倍对齐;如果该变量是一个 short 型数据,那么编译器就会按2字节或2字节的整数倍边界对齐;如果是一个 char 类型的变量,那么编译器就会按照1字节对齐。

程序示例

#include <stdio.h>

int a1 = 1;
int a2 = 2;
char c1 = 'M';
char c2 = 'F';

int main(void)
{
    printf("a1: %p\n", &a1);
    printf("a2: %p\n", &a2);
    printf("c1: %p\n", &c1);
    printf("c2: %p\n", &c2);

    return 0;
}

执行结果

[email protected]:~/tmp$ gcc test.c 
[email protected]:~/tmp$ ./a.out  
a1: 0x5587a10e2010
a2: 0x5587a10e2014
c1: 0x5587a10e2018
c2: 0x5587a10e2019

通过运行结果我们可以看到,对于 int 型数据,其在内存中的地址都是以4字节或4字节整数倍对齐的。而 char 类型的数据,其在内存中是以1字节对齐的。变量 c2 就直接分配到了 c1 变量的下一个存储单元,不用像 int 数据那样考虑4字节对齐。接下来,我们修改一下程序,指定变量 c2 按4字节对齐。

程序示例

#include <stdio.h>

int a1 = 1;
int a2 = 2;
char c1 = 'M';
char c2 __attribute__((aligned(4))) = 'F';

int main(void)
{
    printf("a1: %p\n", &a1);
    printf("a2: %p\n", &a2);
    printf("c1: %p\n", &c1);
    printf("c2: %p\n", &c2);

    return 0;
}

执行结果

[email protected]:~/tmp$ ./a.out  
a1: 0x557fafac8010
a2: 0x557fafac8014
c1: 0x557fafac8018
c2: 0x557fafac801c

通过运行结果可以看到,字符变量 c2 由于使用 aligned 属性声明按照4字节边界对齐,所以编译器不可能再给其分配 0x00402009 这个地址,因为这个地址不是4字节对齐的。编译器空出3个字节单元,直接从 0x0040200C 这个地址上给变量 c2 分配存储空间。

02. 数据需要对齐的原因

通过 aligned 这个属性声明,我们虽然可以显式指定变量的地址对齐方式,但是也会因边界对齐造成一定的内存空洞,浪费一定的内存空间。比如在上面这个程序中,0x557fafac8019~0x557fafac801b这三个地址空间的存储单元就没有被使用。

既然地址对齐会造成一定的内存空洞,那我们为什么还要按照这种对齐方式去存储数据呢?一个主要原因就是,这种对齐设置可以简化 CPU 和内存 RAM 之间的接口和硬件设计。比如一个32位的计算机系统,CPU 读取内存时,硬件设计上可能只支持4字节或4字节倍数对齐的地址访问,CPU 每次往内存 RAM 读写数据时,一个周期可以读写4个字节。如果我们把一个数据放在4字节对齐的地址上,那么CPU一次就可以把数据读写完毕;如果我们把一个 int 型数据放在一个非4字节对齐的地址上,那 CPU 就要分2次才能把这个4字节大小的数据读写完毕。

为了配合计算机的硬件设计,编译器在编译程序时,对于一些基本数据类型,比如 int、char、short、float 等,会按照其数据类型的大小进行地址对齐,按照这种地址对齐方式分配的存储地址,CPU 一次就可以读写完毕。虽然边界对齐会造成一些内存空洞,浪费一些内存单元,但是在硬件上的设计却大大简化了。这也是编译器给我们定义的变量分配地址时,不同类型变量按不同字节数地址对齐的原因。

除了 int、char、short、float 这些基本类型数据,对于一些复合类型数据,也要满足地址对齐要求。

03. 属性声明: aligned

GNU C 通过 attribute 来声明 aligned 和 packed 属性,指定一个变量或类型的对齐方式。这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址。如果你想定义一个变量,在内存中以8字节地址对齐,就可以这样定义。

int var __attribute__((aligned(8));

通过 aligned 属性,我们可以直接显式指定变量 var 在内存中的地址对齐方式。aligned 有一个参数,表示要按几字节对齐,使用时要注意地址对齐的字节数必须是2的幂次方,否则编译就会出错。

错误示例

#include <stdio.h>

int main(void)
{
    int var __attribute__((aligned(3)));

    var = 3;

    return 0;
}

执行结果

[email protected]:~/tmp$ gcc test.c  
test.c: In function ‘main’:
test.c:5:5: error: requested alignment ‘3’ is not a positive power of 2
    5 |     int var __attribute__((aligned(3)));
      |     ^~~

04. 结构体对齐

结构体作为一种复合数据类型,编译器在给一个结构体变量分配存储空间时,不仅要考虑结构体内各个基本成员的地址对齐,还要考虑结构体整体的对齐。为了结构体内的成员地址对齐,编译器可能会在结构体内填充一些空间;为了结构体整体对齐,编译器可能会在结构体的末尾填充一些空间。

接下来,我们定义一个结构体,结构体内定义 int、char 和 short 三种成员,并打印结构体的大小和各个成员的地址。

程序示例

#include <stdio.h>

struct student
{
    char a;
    int b;
    short c;
};


int main(void)
{
    struct student s;

    printf("sizeof(s): %lu\n", sizeof(s));
    printf("a: %p\n", &s.a);
    printf("b: %p\n", &s.b);
    printf("c: %p\n", &s.c);


    return 0;
}

执行结果

[email protected]:~/tmp$ gcc test.c  
[email protected]:~/tmp$ ./a.out 
sizeof(s): 12
a: 0x7ffdc37394bc
b: 0x7ffdc37394c0
c: 0x7ffdc37394c4

我们可以看到,因为结构体的成员 b 需要4字节对齐,编译器在给成员 a 分配完空间后,接着会空出3个字节,在满足4字节对齐的 0x7ffdc37394c0地址处才给成员 b 分配存储空间。接着是 short 类型的成员 c 占据2字节的存储空间。三个结构体成员一共占据4+4+2=10字节的存储空间,根据结构体的对齐规则,结构体的整体对齐要向结构体所有成员中最大对齐字节数或其整数倍对齐,或者说结构体的整体长度要为其最大成员字节数的整数倍,如果不是整数倍要补齐。因为结构体最大成员 int 为4个字节,或者说按4字节的整数倍对齐,所以结构体的长度要为4的整数倍,要在结构体的末尾补充2个字节,所以最后结构体的 size 为12个字节。

结构体成员中,不同的排放顺序,可能也会导致结构体的整体长度不一样,我们修改一下上面的程序。

程序示例

#include <stdio.h>

struct student
{
    char a;
    short b;
    int c;
};


int main(void)
{
    struct student s;

    printf("sizeof(s): %lu\n", sizeof(s));
    printf("a: %p\n", &s.a);
    printf("b: %p\n", &s.b);
    printf("c: %p\n", &s.c);


    return 0;
}

执行结果

[email protected]:~/tmp$ gcc test.c  
[email protected]:~/tmp$ ./a.out  
sizeof(s): 8
a: 0x7ffd7f045c60
b: 0x7ffd7f045c62
c: 0x7ffd7f045c64

我们调整了一些成员顺序,你会发现,char 型变量 a 和 short 型变量 b,分配在了结构体的前4个字节存储空间中,而且都满足各自的地址对齐,整个结构体大小是8字节,只造成一个字节的内存空洞。我们继续修改程序,让 short 型的变量 b 按4字节对齐:

程序示例

#include <stdio.h>

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


int main(void)
{
    struct student s;

    printf("sizeof(s): %lu\n", sizeof(s));
    printf("a: %p\n", &s.a);
    printf("b: %p\n", &s.b);
    printf("c: %p\n", &s.c);


    return 0;
}

执行结果

[email protected]:~/tmp$ gcc test.c  
[email protected]:~/tmp$ ./a.out  
sizeof(s): 12
a: 0x7ffc5dc269dc
b: 0x7ffc5dc269e0
c: 0x7ffc5dc269e4

你会发现,结构体的大小又重新变为12个字节。这是因为,我们显式指定 short 变量以4字节地址对齐,导致变量 a 的后面填充了3个字节空间。int 型变量 c 也要4字节对齐,所以变量 b 的后面也填充了2个字节,导致整个结构体的大小为12字节。

我们不仅可以显式指定结构体内某个成员的地址对齐,也可以指定整个结构体的对齐方式

程序示例

#include <stdio.h>

struct student
{
    char a;
    short b;
    int c;
} __attribute__((aligned(16)));


int main(void)
{
    struct student s;

    printf("sizeof(s): %lu\n", sizeof(s));
    printf("a: %p\n", &s.a);
    printf("b: %p\n", &s.b);
    printf("c: %p\n", &s.c);


    return 0;
}

执行结果

[email protected]:~/tmp$ gcc test.c  
[email protected]:~/tmp$ ./a.out  
sizeof(s): 16
a: 0x7ffe81c5e530
b: 0x7ffe81c5e532
c: 0x7ffe81c5e534

在这个结构体中,各个成员一共占8个字节。通过前面学习我们知道,整个结构体的对齐只要是最大成员对齐字节数的整数倍即可。所以这个结构体整体就以8字节对齐,结构体的整体长度为8字节。但是我们在这里,显式指定结构体整体以16字节对齐,所以编译器就会在这个结构体的末尾填充8个字节以满足16字节对齐的要求,导致结构体的总长度变为16字节。

05. 附录

参考:C语言嵌入式Linux高级编程