简介
在计算机的世界里最常见的问题就是对齐访问,对于CPU而言如果一次内存访问时这个地址不是对齐的,那么它需要做对齐措施,以保证这一次的数据是正确的,不会产生数据混乱的情况,这样带来的情况就是低效率,原本可能两个时钟周期就可以完成的读写操作,因为是非对齐的地址CPU需要更多的时钟周期来处理数据,如果这套处理逻辑将执行上亿次那么所带来的性能损耗是非常巨大的。
另外许多CPU都有非对齐地址访问限制,例如访问0x03这类非对齐地址就会触发Data Abort错误导致程序被系统异常结束掉,但有时我们的确需要访问一些非对齐的地址时Linux内核为我们提供了通用访问接口get_unaligned、put_unaligned
实现原理与用法
get_unaligned定义在include/linux/unaligned.h约第13行左右,下面是它的原型:
#define get_unaligned(ptr) __get_unaligned_t(typeof(*(ptr)), (ptr))
__get_unaligned_t的定义在include/vdso/unaligned.h约第5行左右:
#define __get_unaligned_t(type, ptr) ({ \
const struct { type x; } __packed *__pptr = (typeof(__pptr))(ptr); \
__pptr->x; \
})
上面代码没有太多复杂的地方,它的根本核心思想在于利用编译器的__packed属性,__packed是内核做的封装,是GCC编译器属性:
#define __packed __attribute__((packed))
这条属性是告诉GCC该结构体采用的是紧凑型对齐,不要做任何对齐扩充,这条属性有个要点,就是它只能给结构体使用,但如果结构体加上了这条属性GCC编译器会根据架构来保证对该结构体的访问一定是紧凑的,因为该属性的结构体里面所有的成员都是紧凑的那么可能某个成员它所处的地址并不是对齐的,那么GCC会保证对该结构体的访问是正确的,避免出现Data Abort的情况,因为GCC编译器是由后端和前端两部分组成的,后端是根据目标架构决定的,例如Arm、X86,会使用不同的指令集来编译你的代码,当你的架构支持非对齐访问时那么GCC对使用attribute((packed))属性的结构体访问时会像正常访问对齐地址那样去访问,无论里面是否存在非对齐的成员变量,因为CPU架构可以处理这个问题,只是会带来性能上的损耗而已,但是如果架构不支持非对齐访问的话,那么GCC编译器会去生成1字节访问代码,因为任何架构、总线天然支持1字节地址访问,这与遍历byte类型一样,可以字段选择天然访问1字节地址数据,只是效率也会下降,但是这样保证了不会出现Data Abort这样的硬件错误。
所以Linux内核其实是通过__attribute__((packed))属性将问题巧妙的让GCC编译器来帮我们解决这些问题,所以Linux也被称为GNU/Linux。
下面我将通过汇编代码来展示这一特性,下面是C语言访问代码:
int main() {
unsigned char data[5] = {0x01, 0x02, 0x03, 0x04, 0x05};
unsigned int *ptr = (unsigned int *)(data + 1);
unsigned int value = __get_unaligned_t(unsigned int, ptr);
printf("Value: 0x%x\n", value);
return 0;
}
上面这段代码会尝试对一个非对齐的内存地址进行访问,下面将它翻译成汇编可以看到如下代码(这里博主使用的编译器是arm-none-linux-gnueabihf-gcc):
main:
@ args = 0, pretend = 0, frame = 24
@ frame_needed = 1, uses_anonymous_args = 0
push {r7, lr}
sub sp, sp, #24
add r7, sp, #0
movw r2, #:lower16:.LC0
movt r2, #:upper16:.LC0
adds r3, r7, #4
ldm r2, {r0, r1}
str r0, [r3]
adds r3, r3, #4
strb r1, [r3]
adds r3, r7, #4
adds r3, r3, #1
str r3, [r7, #20]
ldr r3, [r7, #20]
str r3, [r7, #16]
ldr r3, [r7, #16]
ldr r3, [r3] @ unaligned
str r3, [r7, #12]
ldr r1, [r7, #12]
movw r0, #:lower16:.LC1
movt r0, #:upper16:.LC1
bl printf
movs r3, #0
mov r0, r3
adds r7, r7, #24
mov sp, r7
重点是这两段代码:
ldr r3, [r7, #16]
ldr r3, [r3] @ unaligned
可以看到i编译器已经给出unaligned注释了,ldr r3, [r7, #16]是将要访问的地址存到r3寄存器里,然后在将其读取到r3寄存器,这一套操作非常连贯,说明当前架构是支持非对齐访问的,它直接用了ldr来加载,但如果此时架构不支持怎么办?可以通过-mno-unaligned-access参数来强制让编译器以不支持非对齐访问为前提生成代码:
main:
@ args = 0, pretend = 0, frame = 24
@ frame_needed = 1, uses_anonymous_args = 0
push {r7, lr}
sub sp, sp, #24
add r7, sp, #0
movw r2, #:lower16:.LC0
movt r2, #:upper16:.LC0
adds r3, r7, #4
ldm r2, {r0, r1}
str r0, [r3]
adds r3, r3, #4
strb r1, [r3]
adds r3, r7, #4
adds r3, r3, #1
str r3, [r7, #20]
ldr r3, [r7, #20]
str r3, [r7, #16]
ldr r3, [r7, #16]
ldrb r2, [r3] @ zero_extendqisi2
ldrb r1, [r3, #1] @ zero_extendqisi2
lsls r1, r1, #8
orrs r2, r2, r1
ldrb r1, [r3, #2] @ zero_extendqisi2
lsls r1, r1, #16
orrs r2, r2, r1
ldrb r3, [r3, #3] @ zero_extendqisi2
lsls r3, r3, #24
orrs r3, r3, r2
str r3, [r7, #12]
ldr r1, [r7, #12]
movw r0, #:lower16:.LC1
movt r0, #:upper16:.LC1
bl printf
movs r3, #0
mov r0, r3
adds r7, r7, #24
mov sp, r7
可以清楚的看到代码量增加了,重点代码在这:
ldr r3, [r7, #16]
ldrb r2, [r3] @ zero_extendqisi2
ldrb r1, [r3, #1] @ zero_extendqisi2
lsls r1, r1, #8
orrs r2, r2, r1
ldrb r1, [r3, #2] @ zero_extendqisi2
lsls r1, r1, #16
orrs r2, r2, r1
ldrb r3, [r3, #3] @ zero_extendqisi2
lsls r3, r3, #24
orrs r3, r3, r2
可以看到它用的是ldrb指令而非ldr,ldrb是从内存中加载一个字节,lsls指令是左移指令,整个汇编代码相当于从内存中读取1字节放到r2,在读取1字节放到r1,然后在通过lsls左移,在通过orrs进行按位或合并两个寄存器,整个过程就在做一件事情:反复4次将4字节读取出来,然后拼接在一起,反复四次完成非对齐读取操作。
Linux内核get_unaligned原理解析
3693

被折叠的 条评论
为什么被折叠?



