Linux内核里get_unaligned原理和用法

Linux内核get_unaligned原理解析

简介

在计算机的世界里最常见的问题就是对齐访问,对于CPU而言如果一次内存访问时这个地址不是对齐的,那么它需要做对齐措施,以保证这一次的数据是正确的,不会产生数据混乱的情况,这样带来的情况就是低效率,原本可能两个时钟周期就可以完成的读写操作,因为是非对齐的地址CPU需要更多的时钟周期来处理数据,如果这套处理逻辑将执行上亿次那么所带来的性能损耗是非常巨大的。
另外许多CPU都有非对齐地址访问限制,例如访问0x03这类非对齐地址就会触发Data Abort错误导致程序被系统异常结束掉,但有时我们的确需要访问一些非对齐的地址时Linux内核为我们提供了通用访问接口get_unalignedput_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编译器是由后端和前端两部分组成的,后端是根据目标架构决定的,例如ArmX86,会使用不同的指令集来编译你的代码,当你的架构支持非对齐访问时那么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字节读取出来,然后拼接在一起,反复四次完成非对齐读取操作。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

17岁boy想当攻城狮

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值