Linux内核完全注释 阅读笔记:3.3、C语言程序
<p style="text-indent: 2em;">本小节给出内核中经常用到的一些gcc扩充语句的说明。</p><p class="artical_littlestyle1">1、C程序编译和链接</p><p style="text-align:center">使用gcc编译器编译C语言程序时,通常会经过4个处理阶段,即<span style="color: rgb(0, 112, 192);">预处理阶段</span>、<span style="color: rgb(0, 112, 192);">编译阶段</span>、<span style="color: rgb(0, 112, 192);">汇编阶段</span>和<span style="color: rgb(0, 112, 192);">链接阶段</span>,如下图所示:<img src="/uploads/AilsonJack/2018.09.18/1537271440151479.png" onclick="preview_image('/uploads/AilsonJack/2018.09.18/1537271440151479.png')"/></p><p style="text-indent: 0em;">gcc的命令行格式如下:<br/></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">gcc [选项] [-o outfile] infile…</span><br/></p><p style="text-indent: 2em;">infile是输入的C语言文件,outfile是编译产生的输出文件。对于某次编译过程,并非一定要全部执行这4个阶段,使用命令行选项可以让gcc编译过程在某个处理阶段后就停止执行。例如,使用’-S’选项可以让gcc在输出了C程序对应的汇编语言程序之后就停止运行;使用’-c’选项可以让gcc只生成目标文件(.o后缀的文件)而不执行链接处理。<br/></p><p class="artical_littlestyle2">2、嵌入汇编</p><p style="text-indent: 2em;">本节介绍内核C语言程序中接触到的嵌入式汇编(内联汇编)语句。具有输入和输出参数的嵌入式汇编语句的基本格式如下:<br/><span style="color: rgb(0, 112, 192);">asm(“汇编语句”<br/> : 输出寄存器<br/> : 输入寄存器<br/> : 会被修改的寄存器);</span><br/></p><p style="text-indent: 2em;">除第1行以外,后面带冒号的行若不使用就都可以省略。其中,”<span style="color: rgb(255, 0, 0);">asm</span>”是内联汇编语句的关键词;”<span style="color: rgb(0, 112, 192);">汇编语句</span>”是你写汇编指令的地方。<br/></p><p style="text-indent: 2em;">”<span style="color: rgb(0, 112, 192);">输出寄存器</span>”表示当这段嵌入式汇编执行完之后,哪些寄存器用来存放输出数据,这些寄存器会分别对应一C语言表达式值或一个内存地址。<br/></p><p style="text-indent: 2em;">”<span style="color: rgb(0, 112, 192);">输入寄存器</span>”表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值。<br/></p><p style="text-indent: 2em;">”<span style="color: rgb(0, 112, 192);">会被修改的寄存器</span>”表示你已对其中列出的寄存器中的值进行了改动,gcc编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,gcc需要重新加载这些寄存器。因此我们需要把那些没有在输出/输入寄存器部分列出,但是在汇编语句中明确使用到或隐含使用到的寄存器名列在这部分。<br/>下面列出了<span style="color: rgb(0, 112, 192);">kernel/traps.c</span>文件中第22行开始的一段代码作为例子来进行说明:</p><p style="text-indent: 0em;"><span style="color: rgb(0, 112, 192);">#define get_seg_byte(seg,addr) \<br/>({ \<br/>register char __res; \ //定义了一个寄存器变量__res<br/>__asm__("push %%fs; \ //首先保存了fs寄存器原值(段选择符)<br/> mov %%ax,%%fs; \ //然后用seg设置fs<br/> movb %%fs:%2,%%al; \ //取seg:addr处1字节内容到al寄存器中<br/> pop %%fs" \ //恢复fs寄存器原内容<br/> :"=a" (__res) \ //输出寄存器列表<br/> :"0" (seg),"m" (*(addr))); \ //输入寄存器列表<br/>__res;})</span><br/></p><p style="text-indent: 2em;">这段10行代码定义了一个嵌入式汇编语言宏函数。通常使用汇编语句最方便的方式是把它们放在一个宏内。<span style="color: rgb(255, 0, 0);">用圆括号括住的组合语句(花括号中的语句):”({})”可以作为表达式使用</span>,其中最后一行上的变量__res(第10行)是该表达式的输出值,见下一节说明。<br/></p><p style="text-indent: 2em;">因为宏语句需要在一行上,因此这里使用反斜杠’\’将这些语句连接成一行。这条宏定义将被替换到程序中引用该宏名称的地方。第1行定义了宏名称,也即是宏函数名称get_seg_byte(seg,addr)。第3行定义了一个寄存器变量__res,该变量将被保存在一个寄存器中,以便于快速访问和操作。<span style="color: rgb(255, 0, 0);">如果想指定寄存器(例如eax),那么我们可以把该句写成”register char __res asm(“ax”);”</span>,其中’asm’也可以写成’__asm__’。第4行上的’__asm__’表示嵌入汇编语句的开始。从第4行到第7行的4条语句是AT&T格式的汇编语句。另外,<span style="color: rgb(255, 0, 0);">为了让gcc编译产生的汇编语言程序中寄存器名称前有一个百分号”%”,在嵌入式汇编语句寄存器名称前就必须写上两个百分号”%%”</span>。<br/></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">第8行即是输出寄存器,这句的含义是在这段代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值。”=a”中的”a”称为加载代码,”=”表示这是输出寄存器,并且其中的值将被输出值替代。<br/></span></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">第9行表示在这段代码开始运行时,将seg放到eax寄存器中,”0”表示使用与上面同个位置的输出相同的寄存器。而(*(addr))表示一个内存偏移地址值。为了在上面汇编语句中使用该地址值,嵌入汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以”%0”开始,分别记为%0、%1、…、%9。因此,输出寄存器的编号是%0(这里只有一个输出寄存器),输入寄存器前一部分(“0”(seg))的编号是%1,而后部分的编号是%2。上面第6行上的%2即代表(*(addr))这个内存偏移量。</span><br/></p><p style="text-indent: 2em;">该宏函数的功能是从指定段和偏移值的内存地址处取一个字节。下面在看一个例子:<br/><span style="color: rgb(0, 112, 192);">asm(“cld\n\t”<br/> “rep\n\t”<br/> “stol”<br/> : /*没有输出寄存器*/<br/> : “c”(count-1), “a”(fill_value), “D”(dest)<br/> : “%ecx”, “edi%”);</span><br/></p><p style="text-indent: 2em;">第4行说明这段嵌入汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx寄存器中(加载代码是”c”),fill_value加载到eax中,dest放到edi中。表3-4中是一些你可能会用到的寄存器加载代码及其具体的含义:<br/></p><p style="text-align:center"><img src="/uploads/AilsonJack/2018.09.18/1537271440114489.png" onclick="preview_image('/uploads/AilsonJack/2018.09.18/1537271440114489.png')"/></p><p style="text-indent: 2em;">下面的例子不是让你自己指定哪个变量使用哪个寄存器,而是让gcc为你选择。<br/><span style="color: rgb(0, 112, 192);">asm(“leal (%1,%1,4), %0”<br/> : “=r”(y)<br/> : “0”(x));</span><br/></p><p style="text-indent: 2em;">“leal (r1, r2, 4), r3”语句表示:(r1+r2*4) -> r3。如果gcc将r指定为eax的话,那么上面汇编语句的含义为:<br/></p><p style="text-indent: 2em;">“leal (eax,eax,4), eax”<br/></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">注意:在执行代码时,如果不希望汇编语句被gcc优化而作修改,就需要在asm符号后面添加关键词volatile</span>,下面列出了两种声明方式:<br/></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">asm volatile (...);</span><br/></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">__asm__ __volatile__ (...);</span> <span style="color: rgb(0, 112, 192);">//建议使用这种方式,兼容性更好</span><br/></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">关键词volatile也可以放在函数名前来修饰函数,用来通知gcc编译器该函数不会返回,即该函数不会返回到调用者代码中(例如do_exit()函数)</span>。<br/></p><p style="text-indent: 2em;">下面看一个比较长的例子,该代码是从<span style="color: rgb(0, 112, 192);">include/string.h</span>文件中摘取的,是strncmp()字符串比较函数的一种实现。其中每行中的”\n\t”是用于gcc预处理程序输出列表好看而设置的。<br/><span style="color: rgb(0, 112, 192);">//字符串1和字符串2的前count个字符进行比较<br/>//参数: cs:字符串1, ct:字符串2, count:比较的字符数<br/>//%0:eax(__res)返回值, %1:edi(cs)串1指针, %2:esi(ct)串2指针, %3:ecx(count)<br/>//返回:如果串1>串2,则返回1; 串1==串2,则返回0; 串1<串2,则返回-1<br/>extern inline int strncmp(const char * cs,const char * ct,int count)<br/>{<br/>register int __res __asm__("ax"); //__res是寄存器变量,该变量保存在寄存器eax中<br/>__asm__("cld\n" //清方向位<br/> "1:\tdecl %3\n\t" //count--<br/> "js 2f\n\t" //如果count<0,则向前跳转到标号2<br/> "lodsb\n\t" //取串2的字符ds:[esi]->al,并且esi++<br/> "scasb\n\t" //比较al与串1的字符es:[edi],并且edi++<br/> "jne 3f\n\t" //如果不相等,则向前跳转到标号3<br/> "testb %%al,%%al\n\t" //该字符是NULL字符吗?<br/> "jne 1b\n" //不是,则向后跳转到标号1,继续比较<br/> "2:\txorl %%eax,%%eax\n\t" //是NULL字符,则eax清零(返回值)<br/> "jmp 4f\n" //向前跳转到标号4,结束<br/> "3:\tmovl $1,%%eax\n\t" //eax中置1<br/> "jl 4f\n\t" //如果前面比较中,串2字符<串1字符,则返回1,结束<br/> "negl %%eax\n" //否则eax=-eax,返回负值,结束<br/> "4:"<br/> :"=a" (__res):"D" (cs),"S" (ct),"c" (count):"si","di","cx");<br/>return __res; //返回比较结果<br/>}</span><br/></p><p class="artical_littlestyle3">3、圆括号中的组合语句</p><p style="text-indent: 2em;">花括号对”{...}”用于把变量声明和语句组合成一个复合语句(组合语句)或一个语句块,这样在语义上这些语句就等同于一条语句,组合语句的右花括号后面不需要使用分号。<br/></p><p style="text-indent: 2em;">圆括号中的组合语句,即形如”({...})”的语句,可以在GNU C中用作一个表达式使用。这样就可以在表达式中使用loop、switch语句和局部变量,因此这种形式的语句通常称为语句表达式。语句表达式具有如下示例的形式:<br/><span style="color: rgb(0, 112, 192);">({int y = foo(); int z;<br/> if(y > 0) z = y;<br/> else z = -y;<br/> 3 + z;<br/>})</span><br/></p><p style="text-indent: 2em;"><span style="color: rgb(255, 0, 0);">其中组合语句中最后一条语句必须是后面跟随一个分号的表达式。这个表达式(“3+z”)的值即用作整个圆括号括住语句的值。如果最后一条语句不是表达式,那么整个语句表达式就具有void类型,因此没有值</span>。这种表达式中语句声明的任何局部变量都会在整个语句结束后失效。<br/></p><p class="artical_littlestyle4">4、寄存器变量</p><p style="text-indent: 2em;">GNU 对C语言的另一个扩充是允许我们把一些变量值放到CPU寄存器中,即所谓的寄存器变量。寄存器变量可以分为2种:全局寄存器变量和局部寄存器变量。<br/></p><p style="text-indent: 2em;">在GNU C程序中我们可以在函数中用如下形式定义一个局部寄存器变量:<br/></p><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);">register int res __asm__(“ax”);</span><br/></p><p style="text-indent: 2em;">这里ax是变量res所希望使用的寄存器。<br/></p><p class="artical_littlestyle1">5、内联函数</p><p style="text-indent: 2em;">在程序中,通过把一个函数声明为内联(inline)函数,就可以让gcc把函数的代码集成到调用该函数的代码中去。这样处理可以去掉函数调用时进入/退出时间开销,从而肯定能够加快执行速度。因此把一个函数声明为内联函数的主要目的就是能够尽量快速的执行函数体。<br/></p><p style="text-indent: 2em;">内联函数嵌入调用者代码中的操作是一种优化操作,因此只有进行优化编译时才会执行代码嵌入处理。若编译过程中没有使用优化选项”- 0”,那么内联函数的代码就不会被真正的嵌入到调用者代码中,而是只作为普通函数调用来处理。<br/></p><p style="text-indent: 2em;">把一个函数声明为内联函数的方法是在函数声明中使用关键词’inline’,例如内核文件<span style="color: rgb(0, 112, 192);">fs/inode.c</span>中的如下函数:<br/></p><pre class="brush:cpp;toolbar:false PrismJs">inline int inc(int *a)
{
(*a)++;
}</pre><p style="text-indent: 2em;">函数中的某些语句用法可能会使得内联函数的替换操作无法正常进行,或者不适合进行替换操作。例如使用了可变参数、内存分配函数malloca()、可变长度数据类型变量、非局部goto语句、以及递归函数。<span style="color: rgb(255, 0, 0);">编译时可以使用选项-Winline让gcc对标志成inline但不能被替换的函数给出警告信息以及不能替换的原因</span>。<br/></p><p style="text-indent: 2em;">ISO标准C99的内联函数语义定义等同于使用组合关键词inline和static的定义,即省略了关键词”static”。若在程序中需要使用C99标准的语义,那么就需要使用编译选项-std=gnu99。不过为了兼容起见,在这种情况下还是最好使用inline和static组合。<br/></p><p style="text-indent: 2em;"><span style="color: rgb(0, 112, 192);">关键词inline和extern组合在一起的作用几乎类同一个宏定义</span>。使用这种组合方式就是把带有组合关键词的一个函数定义放在.h头文件中,并且把不含关键词的另一个相同函数定义放在一个库文件中。此时头文件中的定义会让绝大数对该函数的调用被替换嵌入。如果还有没有被替换的对该函数的调用,那么就会使用程序文件中或库中的拷贝。Linux 0.1.x内核源代码中文件include/string.h、lib/strings.c就是这种使用方式的一个例子。例如,string.h中定义了如下函数:<br/></p><pre class="brush:cpp;toolbar:false PrismJs">//将字符串(src)拷贝到另一字符串(dest),直到遇到NULL字符后停止
//参数:dest:目的字符串指针 src:源字符串指针, %0:esi(src) %1:edi(dest)
extern inline char * strcpy(char * dest,const char *src)
{
__asm__("cld\n" //清方向位
"1:\tlodsb\n\t" //加载DS:[esi]处1字节->al,并更新esi
"stosb\n\t" //存储字节al->ES:[edi],并更新edi
"testb %%al,%%al\n\t" //刚存储的字节是0?
"jne 1b" //不是,则向后跳转到标号1处,否则结束
::"S" (src),"D" (dest):"si","di","ax");
return dest; //返回目的字符串指针
}</pre><p style="text-indent: 2em;">而在内核函数库目录中,lib/strings.c文件把关键词inline和extern都定义为空,如下所示。因此实际上就在内核函数库中又包含了string.h文件所有这类函数的一个拷贝,即又对这些函数重新定义了一次,并且”消除”了两个关键词的作用。</p><pre class="brush:cpp;toolbar:false PrismJs">#define extern //定义为空
#define inline //定义为空
#define __LIBRARY__
#include <string.h></pre><p style="text-indent: 2em;">此时库函数中重新定义的上述strcpy()函数变成如下形式:</p><pre class="brush:cpp;toolbar:false PrismJs">char * strcpy(char * dest,const char *src) //去掉了关键词inline和extern
{
__asm__("cld\n" //清方向位
"1:\tlodsb\n\t" //加载DS:[esi]处1字节->al,并更新esi
"stosb\n\t" //存储字节al->ES:[edi],并更新edi
"testb %%al,%%al\n\t" //刚存储的字节是0?
"jne 1b" //不是,则向后跳转到标号1处,否则结束
::"S" (src),"D" (dest):"si","di","ax");
return dest; //返回目的字符串指针
}</pre>
你可能也喜欢:
暂无评论,要不要来个沙发
发表评论
JLink V9掉固件修复(灯不亮) 3Zephyr笔记2 - 在STM32F429上运行HelloWorld 2计算NandFlash要传入的行地址和列地址 1Linux MMC子系统 - 6.eMMC 5.1工作模式-设备识别模式 0Linux MMC子系统 - 5.eMMC 5.1工作模式-引导模式 0Linux MMC子系统 - 4.eMMC 5.1常用命令说明(2) 0
标签云
Linux嵌入式实用技巧ARM内核学习问题集合CC++编程语言阅读笔记汇编Linux内核完全注释Windows驱动开发计算机基础ARM11ARMv7-ASTM32IDESublimeLinux内核学习eMMCMMC子系统Ubuntu操作系统OfficeVMWareAPUEgccRTOS中断漫游世界随笔感悟开发工具软件应用编程VsCodearmccarmclang编译器ZephyrSPIJLink网卡驱动安装各种芯片库函数NFSμCOS内核sambaFlashUnix命令与脚本输入法Linux内核设计与实现gitRIFFWAVJATGFTPar8161安装centos有线上网μCGUI字库工程建立右键菜单网络文件系统Firefox百度NTFS文件系统CodeBlocksCentOS数据结构算法PhotoShop51KeilQTUltraEditscanfglibc宏定义UIDGID优先级娱乐天地SourceInsight磁盘扇区总线I2CPDFBComparePythonI2SFPUMakefileSWDCPUARP软件推荐FileZilla