GUN C编译器拓展语法学习笔记(二)属性声明

news2024/12/24 10:19:18

一、属性声明

1、存储段:section

1.1 GNU C编译器扩展关键字:__attribute__

  GNU C增加了一个__attribute__关键字用来声明一个函数、变量或类型的特殊属性。主要用途就是指导编译器在编译程序时进行特定方面的优化或代码检查。例如,我们可以通过属性声明来指定某个变量的数据对齐方式。
  __attribute__的使用非常简单,当我们定义一个函数、变量或类型时,直接在它们名字旁边添加下面的属性声明即可。
在这里插入图片描述
  需要注意的是,__attribute__后面是两对小括号,不能图方便只写一对,否则编译就会报错。括号里面的ATTRIBUTE表示要声明的属性。目前__attribute__支持十几种属性声明。

  • section.
  • aligned.
  • packed.
  • format.
  • weak.
  • alias.
  • noinline.
  • always_inline.

  aligned和packed用来显式指定一个变量的存储对齐方式。在正常情况下,当我们定义一个变量时,编译器会根据变量类型给这个变量分配合适大小的存储空间,按照默认的边界对齐方式分配一个地址。而使用__atttribute__这个属性声明,就相当于告诉编译器:按照我们指定的边界对齐方式去给这个变量分配存储空间。
在这里插入图片描述
  有些属性可能还有自己的参数。如aligned(8)表示这个变量按8字节地址对齐,属性的参数也要使用小括号括起来,如果属性的参数是一个字符串,则小括号里的参数还要用双引号引起来。

1.2 属性声明:section

  section属性的主要作用是:在程序编译时,将一个函数或变量放到指定的段,即放到指定的section中。一个可执行文件主要由代码段、数据段、BSS段构成。除了这三个段,可执行文件中还包含其他一些段。用编译器的专业术语讲,还包含其他一些section,如只读数据段、符号表等。
  在Linux环境下,使用GCC编译生成一个可执行文件a.out,使用readelf命令,就可以查看这个可执行文件中各个section的基本信息,如大小、起始地址等。在这些section中,.text section就是我们常说的代码段,.data section是数据段,.bss section是BSS段。
在这里插入图片描述
在这里插入图片描述
  编译器在编译程序时,以源文件为单位,将一个个源文件编译生成一个个目标文件。在编译过程中,编译器都会按照这个默认规则,将函数、变量分别放在不同的section中,最后将各个section组成一个目标文件。编译过程结束后,链接器会将各个目标文件组装合并、重定位,生成一个可执行文件。
在这里插入图片描述
  在GNU C中,我们可以通过__attribute__section属性,显式指定一个函数或变量,在编译时放到指定的section里面。通过上面的程序我们知道,未初始化的全局变量默认是放在.bss section中的,即默认放在BSS段中。现在我们就可以通过section属性声明,把这个未初始化的全局变量放到数据段.data中。
  通过readelf命令查看符号表,我们可以看到,uninit_val这个未初始化的全局变量,通过__attribute__((section(".data")))属性声明,就和初始化的全局变量一样,被编译器放在了数据段.data中。

1.3 U-boot镜像自复制分析

  有了section这个属性声明,我们就可以试着分析:U-boot在启动过程中,是如何将自身代码加载的RAM中的
  嵌入式Linux中,U-boot的用途主要是加载Linux内核镜像到内存,给内核传递启动参数,然后引导Linux操作系统启动。U-boot一般存储在NOR Flash或NAND Flash上。无论从NOR Flash还是从NAND Flash启动,U-boot其本身在启动过程中,都会从Flash存储介质上加载自身代码到内存,然后进行重定位,跳到内存RAM中去执行。
  那么U-boot是怎么完成代码自复制的呢?或者说它是怎样将自身代码从Flash复制到内存的呢?
  在复制自身代码的过程中,一个主要的疑问就是:U-boot是如何识别自身代码的?是如何知道从哪里开始复制代码的?是如何知道复制到哪里停止的?这时候我们需要了解U-boot源码中的一个零长度数组。
在这里插入图片描述  这两行代码的作用是分别定义一个零长度数组,并指示编译器要分别放在.__image_copy_start和.__image_copy_end这两个section中。
  链接器在链接各个目标文件时,会按照链接脚本里各个section的排列顺序,将各个section组装成一个可执行文件。
在这里插入图片描述在这里插入图片描述
  通过链接脚本我们可以看到,__image_copy_start__image_copy_end这两个section,在链接的时候分别放在了代码段.text的前面数据段.data的后面,作为U-boot复制自身代码的起始地址和结束地址。**而在这两个section中,我们除了放两个零长度数组,并没有放其他变量,众所周知,零长度数组是不占用存储空间的。**因此以上两个零长度数组分别代表了U-boot镜像要复制自身镜像的起始地址和结束地址。无论U-boot自身镜像存储在NOR Flash,还是存储在NAND Flash上,只要知道了这两个地址,我们就可以直接调用相关代码复制。
  在嵌入式系统中,通过ARM的LDR伪指令,直接获取要复制镜像的首地址,并保存在R1寄存器中。数组名本身其实就代表一个地址,通过这种方式,U-boot在嵌入式启动的初始阶段,就完成了自身代码的复制工作:从Flash复制自身镜像到内存中,然后进行重定位,最后跳到内存中执行。

2、属性声明:aligned

2.1 地址对齐:aligned

  GNU C通过__attribute__来声明alignedpacked属性,指定一个变量或类型的对齐方式。这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址。
  定义一个int变量,在内存中以8字节地址对齐,就可以这样定义。
![在这里插入图片描述](https://img-blog.csdnimg.cn/9bf5c7c5a99747d4b956b2ecd86b31c3.png#pic_center  通过aligned属性,我们可以显式地指定变量a在内存中的地址对齐方式。aligned有一个参数,表示要按几字节对齐,使用时要注意,地址对齐的字节数必须是2的幂次方,否则编译就会出错。
  一般情况下,当我们定义一个变量时,编译器会按照默认的地址对齐方式,来给该变量分配一个存储空间地址。
在这里插入图片描述  在上面的程序中,我们分别定义2个int型变量、2个char型变量,然后分别打印它们的地址,运行结果如下。
在这里插入图片描述  对于int型数据,其在内存中的地址都是以4字节或4字节整数倍对齐的。而char类型的数据,其在内存中是以1字节对齐的。变量c2就直接被分配到了c1变量的下一个存储单元,不用像int数据那样考虑4字节对齐。接下来,我们修改一下程序,指定变量c2按4字节对齐。
在这里插入图片描述  程序运行结果如下。
在这里插入图片描述
  运行结果可以看到,字符变量c2由于使用aligned属性声明按照4字节边界对齐,所以编译器不可能再给其分配0x00402009这个地址,因为这个地址不是按照4字节对齐的。编译器会空出3个存储单元,直接从0x0040200C这个地址上给变量c2分配存储空间。通过aligned属性声明,虽然可以显式地指定变量的地址对齐方式,但是也会因边界对齐造成一定的内存空洞,浪费内存资源。如在上面这个程序中,0x00402009~0x0040200b这三个地址上的存储单元就没有被使用。
  问题:地址对齐会造成一定的内存空洞,为什么使用地址对齐?
  原因:这种对齐设置可以简化CPU和内存RAM之间的接口和硬件设计。为了配合计算机的硬件设计,编译器在编译程序时,对于一些基本数据类型,如int、char、short、float等,会按照其数据类型的大小进行地址对齐,按照这种地址对齐方式分配的存储地址,CPU一次就可以读写完毕。虽然边界对齐会造成一些内存空洞,浪费一些内存单元,但是在硬件上的设计却大大简化了。

2.2 结构体对齐:aligned

  结构体作为一种复合数据类型,编译器在给一个结构体变量分配存储空间时,不仅要考虑结构体内各个基本成员的地址对齐,还要考虑结构体整体的对齐。为了结构体内各个成员地址对齐,编译器可能会在结构体内填充一些空间;为了结构体整体对齐,编译器可能会在结构体的末尾填充一些空间。
  举个例子定义一个结构体,结构体内定义int、char和short 3个成员,并打印结构体的大小和各个成员的地址。
在这里插入图片描述程序运行结果如下。
在这里插入图片描述
  正常而言,结构体的成员b需要4字节对齐,所以编译器在给成员a分配完1字节的存储空间后,会空出3字节,在满足4字节对齐的0x0028FF34地址处才给成员b分配4字节的存储空间;接着是short类型的成员c占据2字节的存储空间;三个结构体成员一共占用1+3+4+2=10字节的存储空间。但是根据结构体的对齐规则,**结构体的整体对齐要按结构体所有成员中最大对齐字节数或其整数倍对齐,或者说结构体的整体长度要为其最大成员字节数的整数倍,如果不是整数倍则要补齐。**因为结构体最大成员int为4字节,所以结构体要按4字节对齐,或者说结构体的整体长度要是4的整数倍,要在结构体的末尾补充2字节,最后结构体的大小为12字节。
  结构体成员按不同的顺序排放,可能会导致结构体的整体长度不一样。如下程序所示。
在这里插入图片描述  程序运行结果如下。
在这里插入图片描述  我们发现,char型变量a和short型变量b,被分配在了结构体前4字节的存储空间中,而且都满足各自的地址对齐方式,整个结构体大小是8字节,只造成1字节的内存空洞。我们继续修改程序,让short型的变量b按4字节对齐。
在这里插入图片描述  程序运行结果如下。
在这里插入图片描述
  结构体的大小又重新变为12字节。这是因为,我们显式指定short变量以4字节地址对齐,导致变量a的后面填充了3字节空间。int型变量c也要4字节对齐,所以变量b的后面也填充了2字节,导致整个结构体的大小为12字节。
  我们不仅可以显式指定结构体内某个成员的地址对齐,也可以显式指定整个结构体的对齐方式。
在这里插入图片描述在这里插入图片描述  程序运行结果如下。
在这里插入图片描述  在这个结构体中,各个成员共占8字节。通过前面的学习我们知道,整个结构体的对齐只要按最大成员的对齐字节数对齐即可。所以这个结构体整体就以4字节对齐,结构体的整体长度为8字节。但是在这里,显式指定结构体整体以16字节对齐,所以编译器就会在这个结构体的末尾填充8字节以满足16字节对齐的要求,最终导致结构体的总长度变为16字节。
  问题:编译器一定会按照aligned指定的方式对齐吗?
  回答:非也!我们通过这个属性声明,其实只是建议编译器按照这种大小地址对齐,但不能超过编译器允许的最大值。一个编译器,对每个基本数据类型都有默认的最大边界对齐字节数。如果超过了,则编译器只能按照它规定的最大对齐字节数来给变量分配地址。
在这里插入图片描述  在这个程序中,我们指定char型的变量c2以16字节对齐,编译运行结果如下。
在这里插入图片描述  我们可以看到,编译器给c2分配的地址是按16字节地址对齐的,如果我们继续修改c2变量按32字节对齐,你会发现程序的运行结果不再有变化,编译器仍然分配一个16字节对齐的地址,这是因为32字节的对齐方式已经超过编译器允许的最大值了。

2.3 属性声明:packed

  aligned属性一般用来增大变量的地址对齐,元素之间因为地址对齐会造成一定的内存空洞。而packed属性则与之相反,一般用来减少地址对齐,指定变量或类型使用最可能小的地址对齐方式
在这里插入图片描述  在上面的程序中,我们将结构体的成员b和c使用packed属性声明,就是告诉编译器,尽量使用最可能小的地址对齐给它们分配地址,尽可能地减少内存空洞。程序的运行结果如下。
在这里插入图片描述
  这个特性在底层驱动开发中还是非常有用的。使用packed可以避免这个问题,结构体的每个成员都紧挨着,依次分配存储地址,这样就避免了各个成员因地址对齐而造成的内存空洞。我们也可以对整个结构体添加packed属性,这和分别对每个成员添加packed属性效果是一样的。
在这里插入图片描述

2.4 内核中的aligned、packed声明

  在Linux内核源码中,我们经常看到aligned和packed一起使用,即对一个变量或类型同时使用aligned和packed属性声明。这样做的好处是:既避免了结构体内各成员因地址对齐产生内存空洞,又指定了整个结构体的对齐方式。
在这里插入图片描述  程序运行结果如下。
在这里插入图片描述
  结构体data虽然使用了packed属性声明,结构体内所有成员所占的存储空间为7字节,但是我们同时使用了aligned(8)指定结构体按8字节地址对齐,所以编译器要在结构体后面填充1字节,这样整个结构体的大小就变为8字节,按8字节地址对齐。

3、format

3.1 变参函数的格式检查

  GNU通过__attribute__扩展的format属性,来指定变参函数的参数格式检查。使用方法如下。
在这里插入图片描述
  在一些商业项目中,我们经常会实现一些自定义的打印调试函数,甚至实现一个独立的日志打印模块。这些自定义的打印函数往往是变参函数,用户在调用这些接口函数时参数往往不固定,那么编译器在编译程序时,怎么知道我们的参数格式对不对呢?
  __attribute__format属性这时候就派上用场了。上面的示例代码中,我们定义一个LOG()变参函数,用来实现日志打印功能。
  编译器在编译程序时,如何检查LOG()函数的参数格式是否正确呢?通过给LOG()函数添加__attribute__((format(printf,1,2)))属性声明就可以了。这个属性声明告诉编译器:你知道printf()函数不?你怎么对printf()函数进行参数格式检查的,就按照同样的方法,对LOG()函数进行检查。
  属性format(printf,1,2)有3个参数,第1个参数printf是告诉编译器,按照printf()函数的标准来检查;第2个参数表示在LOG()函数所有的参数列表中格式字符串的位置索引;第3个参数是告诉编译器要检查的参数的起始位置。
在这里插入图片描述
在这个LOG()函数中有2个参数,第1个参数是格式字符串,第2个参数是要打印的一个常量值0,用来匹配格式字符串中的占位符。

4、weak

  在C语言标准中,当我们定义并初始化一个数组时,常用方法如下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/500004.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言三子棋小游戏

哈喽,大家好,今天我们要利用之前所学习的C语言知识来写一个三子棋小游戏。 目录 1.游戏 2.函数部分 2.1.菜单 2.2.初始化棋盘 2.3.打印棋盘 2.4.玩家下棋 2.5.电脑下棋 2.6.判断输赢 2.7.判断棋盘是否已满 3.完整代码展示 1.游戏 今天我们写的…

未知时间信息下雷达运动目标的计算高效重聚焦与估计方法

论文背景 在雷达成像中,回波信号在接收到之前可能已经被多次反射或散射,这样会导致回波信号的时间和频率发生变化。其中,距离向维度上的变化称为距离单元迁移(range cell migration,RCM),频率向…

Spring笔记

文章目录 1、什么是Spring?2、如何创建Spring3、Spring简单的读和取操作1.直接在spring-config.xml里面放置对象2.通过配置扫描路径和添加注解的方式添加Bean对象3.为什么需要五个类注解4.从spring中简单读取 Bean对象5.Resource和Autowired的异同 1、什么是Spring&…

Transformer结构细节

一、结构 Transformer 从大的看由 编码器输入、编码器、解码器、解码器输入和解码器输出构成。 编码器中包含了词嵌入信息编码、位置编码、多头注意力、Add&Norm层以及一个全连接层; 解码器中比编码器多了掩码的多头注意力层。 二、模块 2.1 Input Embeddi…

canvas学习之华丽小球滚动电子时钟

教程来自 4-3 华丽的小球滚动效果 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>华丽小球滚动时钟…

【AVL树的模拟实现】

1 AVL树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。因此&#xff0c;两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决…

人工智能基础部分14-蒙特卡洛方法在人工智能中的应用及其Python实现

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能基础部分14-蒙特卡洛方法在人工智能中的应用及其Python实现&#xff0c;在人工智能领域&#xff0c;蒙特卡洛方法&#xff08;Monte Carlo Method, MCM&#xff09;被广泛应用于各种问题的求解。本文首先将…

wvp-GB28181-pro录像功能开发环境搭建、配置、使用

开发环境、调试环境搭建 开发wvp平台搭建 离线安装脚本&#xff1a;https://gitcode.net/zenglg/ubuntu_wvp_online_install.git 下载离线安装脚本&#xff0c;完成wvp平台的部署 开发环境要求 操作系统&#xff1a;包管理工具是apt ky10桌面版uos桌面版deepin桌面版ubuntu桌面…

ArmDot.NET Crack

ArmDot.NET Crack ArmDot是一个.NET加密工具&#xff0c;用于保护使用.NET编写的程序。 企业需要保护他们的知识产权&#xff0c;包括他们的算法、产品和使用的资源的源代码。 然而&#xff0c;.NET编译器会生成一个通用的可访问代码。代码中嵌入的资源很容易访问&#xff0c;并…

RocketMQ不同的类型消息

目录 普通消息 可靠同步发送 可靠异步发送 单向发送 三种发送方式的对比 顺序消息 事物消息 两个概念 事务消息发送步骤 事务消息回查步骤 消息消费要注意的细节 RocketMQ支持两种消息模式: 普通消息 RocketMQ提供三种方式来发送普通消息&#xff1a;可靠同步发送、…

剑指Offer题集(力扣)

文章目录 剑指Offer题集&#xff08;[力扣题单](https://leetcode.cn/problemset/all/?listIdlcof&page1)&#xff09;[剑指 Offer 03. 数组中重复的数字](https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/)[剑指 Offer 04. 二维数组中的查找](https:…

SSM框架练习一(登录后关联数据表的业务模型)

需要实现的整体功能&#xff1a; 登录反馈信息列表展示查询反馈信息发表反馈 1.数据库设计 创建数据库 创建表结构及其约束 添加测试数据 工具&#xff1a;PHP、Navicat create table tab_user(id int primary key auto_increment,uname varchar(30) not null,pwd varc…

Weblogic XMLDecoder 反序列化漏洞(CVE-2017-10271复现)

文章目录 前言影响版本环境搭建漏洞复现深度利用 前言 CVE-2017-10271漏洞产生的原因大致是Weblogic的WLS Security组件对外提供webservice服务&#xff0c;其中使用了XMLDecoder来解析用户传入的XML数据&#xff0c;在解析的过程中出现反序列化漏洞&#xff0c;导致可执行任意…

从搬砖工到架构师,Java全栈学习路线总结

&#x1f307;文章目录 前言一、前置知识二、 Web前端基础示例&#xff1a;1.文本域2.密码字段 三、后端基础一. Java基础二. 数据库技术三. Web开发技术四. 框架技术五. 服务器部署 四、其他技术五、全栈开发六、综合实践七、学习教程一、前端开发二、后端开发三、数据库开发四…

springboot+jsp乡村中小学校园网站建设

随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;乡村小学校园网当然也不能排除在外&#xff0c;从校园概况、学校风采、招生信息的统计和分析&#xff0c;在过程中会产生大量的…

Maven依赖原则及如何解决Maven依赖冲突

前言 在大数据应用中&#xff0c;现在发现依赖关系非常复杂&#xff0c;在上线之前很长测试&#xff0c;前一段时间在部署udf 出现了导致生产Hiveserver2 宕机问题&#xff0c;出现严重事故。现在就咨询研究一下。Maven虽然已经诞生多年&#xff0c;但仍然是当前最流行的Java系…

Arrays:点燃你的数组操作技巧的隐秘武器。

前言 数组在 Java 中是一种常用的数据结构&#xff0c;用于存储和操作大量数据。但是在处理数组中的数据&#xff0c;可能会变得复杂和繁琐。Arrays 是我们在处理数组时的一把利器。它提供了丰富的方法和功能&#xff0c;使得数组操作变得更加简单、高效和可靠。无论是排序、搜…

【c语言】字符串类型转换 | itoa函数的使用

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ …

MySQL innodb介绍

InnoDB引擎的优点是支持兼容ACID的事务&#xff0c;以及参数完整性&#xff08;即对外键的支持&#xff09;。 Oracle公司2005年10月收购了Innovase&#xff1b;Innobase采用双认证授权。它使用GNU发行&#xff0c;也允许其他想将InnoDB结合到商业软件的团体好的授权 mysql5.…

Java 动态原理详解

Java 动态代理是一种非常重要的编程技术&#xff0c;它在很多场景下都有着广泛的应用。本文将介绍 Java 动态代理的实现原理&#xff0c;并附上相应的源码&#xff0c;以帮助读者更好地理解和应用这一技术。 一、什么是 Java 动态代理&#xff1f; Java 动态代理是一种在运行时…