C硬核:聊聊预编译指令和宏的应用场景(二)

C语言硬件编程栏目

往期文章:
工作数年还是被指针坑了——谈谈C语言指针运算
C硬核:聊聊预编译指令和宏的应用场景(一)



前言

关于预编译指令,在上周已经对重要的几个做了一次介绍。这一期以UEFI源码为蓝本,介绍下#define宏的经典用法。


一、如何方便维护那些值不变的参数?

经常在编写程序的时候,会发现某个常量用的地方很多,而很多初学者都是在每一个使用该常量的地方用数字表示,比如申请长度为8的数组:

int array[8];

而在声明之后使用的过程中对数组元素进行遍历,也是用这个长度8:

for(i=0; i<8; i++){
...
}

在今后代码维护和升级的过程中,可能会发现,这个数组长度要发生变化,那么又需要在声明、定义和使用的过程都需要修改了,这无疑会增加时间成本,甚至还会漏改导致出错。所以我们常用宏来定义这个常量8:

#define ARRAY_SIZE 8

这样我们下次在修改这个数组长度的时候,只需要修改这个宏的值就ok了,这样就实现了常量的统一管理
或许有人说枚举enum也可以实现类似的功能,但从灵活性的角度来讲,宏的优势更加明显!来看看在UEFI kernel中,是怎样定义各种EFI_STATUS的:

///
/// Enumeration of EFI_STATUS.
///@{
#define EFI_SUCCESS               RETURN_SUCCESS
#define EFI_LOAD_ERROR            RETURN_LOAD_ERROR
#define EFI_INVALID_PARAMETER     RETURN_INVALID_PARAMETER
#define EFI_UNSUPPORTED           RETURN_UNSUPPORTED
#define EFI_BAD_BUFFER_SIZE       RETURN_BAD_BUFFER_SIZE
#define EFI_BUFFER_TOO_SMALL      RETURN_BUFFER_TOO_SMALL
#define EFI_NOT_READY             RETURN_NOT_READY
#define EFI_DEVICE_ERROR          RETURN_DEVICE_ERROR
#define EFI_WRITE_PROTECTED       RETURN_WRITE_PROTECTED
#define EFI_OUT_OF_RESOURCES      RETURN_OUT_OF_RESOURCES
#define EFI_VOLUME_CORRUPTED      RETURN_VOLUME_CORRUPTED
#define EFI_VOLUME_FULL           RETURN_VOLUME_FULL
#define EFI_NO_MEDIA              RETURN_NO_MEDIA
#define EFI_MEDIA_CHANGED         RETURN_MEDIA_CHANGED
#define EFI_NOT_FOUND             RETURN_NOT_FOUND
#define EFI_ACCESS_DENIED         RETURN_ACCESS_DENIED
#define EFI_NO_RESPONSE           RETURN_NO_RESPONSE
#define EFI_NO_MAPPING            RETURN_NO_MAPPING
#define EFI_TIMEOUT               RETURN_TIMEOUT
#define EFI_NOT_STARTED           RETURN_NOT_STARTED
#define EFI_ALREADY_STARTED       RETURN_ALREADY_STARTED
#define EFI_ABORTED               RETURN_ABORTED
#define EFI_ICMP_ERROR            RETURN_ICMP_ERROR
#define EFI_TFTP_ERROR            RETURN_TFTP_ERROR
#define EFI_PROTOCOL_ERROR        RETURN_PROTOCOL_ERROR
#define EFI_INCOMPATIBLE_VERSION  RETURN_INCOMPATIBLE_VERSION
#define EFI_SECURITY_VIOLATION    RETURN_SECURITY_VIOLATION
#define EFI_CRC_ERROR             RETURN_CRC_ERROR
#define EFI_END_OF_MEDIA          RETURN_END_OF_MEDIA
#define EFI_END_OF_FILE           RETURN_END_OF_FILE
#define EFI_INVALID_LANGUAGE      RETURN_INVALID_LANGUAGE
#define EFI_COMPROMISED_DATA      RETURN_COMPROMISED_DATA
#define EFI_HTTP_ERROR            RETURN_HTTP_ERROR

#define EFI_WARN_UNKNOWN_GLYPH    RETURN_WARN_UNKNOWN_GLYPH
#define EFI_WARN_DELETE_FAILURE   RETURN_WARN_DELETE_FAILURE
#define EFI_WARN_WRITE_FAILURE    RETURN_WARN_WRITE_FAILURE
#define EFI_WARN_BUFFER_TOO_SMALL RETURN_WARN_BUFFER_TOO_SMALL
#define EFI_WARN_STALE_DATA       RETURN_WARN_STALE_DATA
#define EFI_WARN_FILE_SYSTEM      RETURN_WARN_FILE_SYSTEM
///@}

这些RETURN_SUCCESS/RETURN_LOAD_ERROR/RETURN_INVALID_PARAMETER,可以简单的理解为0/1/2,这里没有使用enum,一个优势就在于我们在查阅code的时候也好查!

二、如何对你的printf/debug语句分级?

编写代码的过程中,可以说printf语句使用的频率是最高的。我们无时无刻不在使用printf或者库中自带的debug方式,对程序运行的过程或结果信息进行输出。但往往会遇到一个问题,自己编写的代码,如果将全部信息都打印出来,未免过多,不利于自己去筛选;而有些信息还是希望在自己调试某个问题的时候展现出来,所以这就涉及一个debug print level的问题。
linux中对此采用面向用户态的分级日志消息来实现,而UEFI kernel中采用DebugPrintErrorLevel来实现,下面以UEFI kernel为例,看看如何对串口输出信息进行分离:

1.定义DebugPrintErrorLevel的划分细则

这里将debug message分为32个等级,分别用PcdDebugPrintErrorLevel这个PCD的每一个Bit来表示,PCD可以简单的理解为一个宏定义:
代码如下(示例):

## This flag is used to control the print out Debug message.<BR><BR>
  #  BIT0  - Initialization message.<BR>
  #  BIT1  - Warning message.<BR>
  #  BIT2  - Load Event message.<BR>
  #  BIT3  - File System message.<BR>
  #  BIT4  - Allocate or Free Pool message.<BR>
  #  BIT5  - Allocate or Free Page message.<BR>
  #  BIT6  - Information message.<BR>
  #  BIT7  - Dispatcher message.<BR>
  #  BIT8  - Variable message.<BR>
  #  BIT10 - Boot Manager message.<BR>
  #  BIT12 - BlockIo Driver message.<BR>
  #  BIT14 - Network Driver message.<BR>
  #  BIT16 - UNDI Driver message.<BR>
  #  BIT17 - LoadFile message.<BR>
  #  BIT19 - Event message.<BR>
  #  BIT20 - Global Coherency Database changes message.<BR>
  #  BIT21 - Memory range cachability changes message.<BR>
  #  BIT22 - Detailed debug message.<BR>
  #  BIT31 - Error message.<BR>
  # @Prompt Debug Message Print Level.
  # @Expression  0x80000002 | (gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel & 0x7F84AA00) == 0
  gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000000|UINT32|0x00000006

2.设计和Error Level相关联的print函数

下面这个函数的第一个参数就指明当前的error level的值,再是format以及VA_LIST:

VOID
EFIAPI
DebugPrint (
  IN  UINTN        ErrorLevel,
  IN  CONST CHAR8  *Format,
  ...
  )
{
  VA_LIST  Marker;

  VA_START (Marker, Format);
  DebugVPrint (ErrorLevel, Format, Marker);
  VA_END (Marker);
}

继续打开DebugVPrint的“盲盒”往下看:

VOID
EFIAPI
DebugVPrint (
  IN  UINTN         ErrorLevel,
  IN  CONST CHAR8   *Format,
  IN  VA_LIST       VaListMarker
  )
{
  DebugPrintMarker (ErrorLevel, Format, VaListMarker, NULL);
}

这里实际的实现函数是DebugPrintMarker,通过比较指定的ErrorLevel和当前宏PcdDebugPrintErrorLevel的大小关系,来决定需不需要再执行buffer的填充和串口数据的输出:

VOID
DebugPrintMarker (
  IN  UINTN         ErrorLevel,
  IN  CONST CHAR8   *Format,
  IN  VA_LIST       VaListMarker,
  IN  BASE_LIST     BaseListMarker
  )
{
  CHAR8    Buffer[MAX_DEBUG_MESSAGE_LENGTH];

  //
  // If Format is NULL, then ASSERT().
  //
  ASSERT (Format != NULL);

  //
  // Check driver debug mask value and global mask
  //
  if ((ErrorLevel & GetDebugPrintErrorLevel ()) == 0) {
    return;
  }

  //
  // Convert the DEBUG() message to an ASCII String
  //
  if (BaseListMarker == NULL) {
    AsciiVSPrint (Buffer, sizeof (Buffer), Format, VaListMarker);
  } else {
    AsciiBSPrint (Buffer, sizeof (Buffer), Format, BaseListMarker);
  }

  //
  // Send the print string to a Serial Port
  //
  SerialPortWrite ((UINT8 *)Buffer, AsciiStrLen (Buffer));
}

3. 调整宏DebugPrintErrorLevel或入口参数errorlevel

从上面code可以看出,要想控制print/debug输出的信息,有以下两种方式:
(1)在使用DebugPrint时,修改error level这个参数,让这个参数在PcdDebugPrintErrorLevel这个某一个Bit上占有一席之地,相与不为0;
(2)如果要减少print/debug输出的信息,可以修改宏DebugPrintErrorLevel,让不为0的bit更少一些,从而关闭某些debug level message的输出。


三、如何批量替换一系列代码?

宏可以替换数值,当然也可以替换一系列执行的语句!
如UEFI kernel中对读写屏障功能代码的宏定义:

//
// GCC inline assembly for Read Write Barrier
//
#define _ReadWriteBarrier() do { __asm__ __volatile__ ("": : : "memory"); } while(0)

为什么要使用do…while(0)呢?不使用可以吗?
答案当然是否定的。假设不使用do…while(0)进行处理,索性替换到原文中,如果原文中出现if/for等分支执行语句,那么本意的整体执行,就导致最后只有某些语句执行,而某些没有被执行的情况。添加do…while(0)让宏里的代码作为整体执行

总结

宏往往搭配预编译指令中的#define一起使用,在C语言使用中无处不在。以上就是常见的几个宏的应用场景,用好宏,让我们工作事半功倍,结构更加清晰!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Cerman

你的鼓励是探索和创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值