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语言使用中无处不在。以上就是常见的几个宏的应用场景,用好宏,让我们工作事半功倍,结构更加清晰!