[汇编与C语言关系]4. 结构体和联合体
用反汇编的方法研究一下C语言的结构体:
#include <stdio.h> int main(int argc, char ** argv) { struct { char a; short b; int c; char d; } s; s.a = 1; s.b = 2; s.c = 3; s.d = 4; printf("%u\n", sizeof(s)); return 0; }
main函数中几条语句的反汇编结果如下:
从访问结构体成员的指令可以看出,结构体的四个成员在栈上是这样排列的:
虽然栈是从高地址向低地址增长的,但结构体成员也是从低地址向高地址排列的,这一点和数组类似。与数组不同的是结构体成员之间不是一个紧挨一个排列的,中间有空隙,称为填充(Padding),不仅如此,在这个结构体的末尾也有三个字节的填充,所以sizeof(s)的值是12。printf的%u转换说明表示无符号数,sizeof的值是siez_t类型的,是某种无符号整形。
为什么编译器要这样处理呢? 大多数计算机体系结构对于访问内存的指令是由限制的,在32位平台上,访问4字节的指令(比如上面的movl)所访问的内存地址应该是4的整数倍,访问两字节的指令(比如上面的movw)所访问的内存地址应该是两字节的整数倍,这称为对齐(Alignment)。如果指令所访问的内存地址没有正确对齐会怎么样呢?在有些平台上将不能访问内存,而是引发一个异常,在x86平台上倒是仍然能访问内存,但是不对齐的指令执行效率比对齐的指令要低,所以编译器在安排各种变量的地址时都会考虑到对齐的问题。对于本例中的结构体,编译器会把它的基地址对齐到4字节边界,也就是说ebp-0x10这个地址一定是4的整数倍。s.a占一个字节,没有对齐的问题。s.b占用两个字节,如果s.b紧挨在s.a后面,它的地址就不能是两字节的整数倍了,所以编译器会在结构体中插入一个填充字节。使s.b的地址也是两字节的整数倍。s.c占4字节,紧挨在s.b的后面就可以了,因为ebp-0xc这个地址也是4的整数倍。那么为什么s.d的后面也要有填充位填充到4字节边界呢?这是为了便于安排这个结构体后面的变量的地址,加入用这种结构体类型组成一个数组,那么后一个结构体只需和前一个结构体紧挨着排列就可以保证它的基地址仍然对齐到4字节边界了,因为在前一个结构体的末尾已经有了填充字节。合理设计结构体个成员的排列顺序可以节省存储空间,例如上例结构体可以改成如下:
struct { char a; char d; short b; int c; } s;
此外gcc提供了一种扩展语法可以消除结构体中的填充字节:
struct { char a; short b; int c; char d; } __attribute__((packed)) s;
这样就不能保证结构体成员对齐了,在访问b和c的时候可能会有效率问题。
以前我们使用的数据类型都是占几个字节,最小的类型也要占一个字节,而在结构体中还可以使用Bit Field语法定义只占几个Bit的成员。
#include <stdio.h> typedef struct { unsigned int one:1; unsigned int two:3; unsigned int three:10; unsigned int four:5; unsigned int :2; unsigned int five:8; unsigned int six:8; } demo_type; int main(void) { demo_type s = { 1, 5, 513, 17, 129, 0x81 }; printf("sizeof demo_type = %u\n", sizeof(demo_type)); printf("values: s=%u,%u,%u,%u,%u,%u\n", s.one, s.two, s.three,s.four, s.five, s.six); return 0; }
s这个结构体的布局如下所示:
Bit Field成员的类型可以是int或unsigned int, 表示有符号数或无符号数,但不表示它像普通的int型一样站4个字节,它后面的数字是几就表示它占多少个Bit,也可以像unsigned int:2这样定义一个未命名的Bit Field,即使不写未命名的Bit Field,编译器也有可能在两个成员之间插入填充位,如上图的five和six之间,这样six这个成员就刚好单独占一个字节了,访问效率会比较高,这个结构体的末尾还填充了3个字节,以便对齐到4字节边界。x86的Byte Order是小端的,从上图中one和two的排列顺序可以看出,如果对一个字节再细分,则字节中的Bit Order也是小端的,因为排在结构体前面的成员(靠近低地址一边的成员)取字节中的低位。Bit Field在驱动程序中是很有用的,因为经常需要单独操作设备寄存器中的一个或几个Bit。