Linux内核完全注释 阅读笔记:3.3、C语言程序
By: Ailson Jack
Date: 2018.09.14
个人博客: http://www.only2fire.com/
本文在我博客的地址是:http://www.only2fire.com/archives/79.html,排版更好,便于学习,也可以去我博客逛逛,兴许有你想要的内容呢。
本小节给出内核中经常用到的一些gcc扩充语句的说明。
1、C程序编译和链接
使用gcc编译器编译C语言程序时,通常会经过4个处理阶段,即预处理阶段、编译阶段、汇编阶段和链接阶段,如下图所示:
gcc的命令行格式如下:
gcc [选项] [-o outfile] infile…
infile是输入的C语言文件,outfile是编译产生的输出文件。对于某次编译过程,并非一定要全部执行这4个阶段,使用命令行选项可以让gcc编译过程在某个处理阶段后就停止执行。例如,使用’-S’选项可以让gcc在输出了C程序对应的汇编语言程序之后就停止运行;使用’-c’选项可以让gcc只生成目标文件(.o后缀的文件)而不执行链接处理。
2、嵌入汇编
本节介绍内核C语言程序中接触到的嵌入式汇编(内联汇编)语句。具有输入和输出参数的嵌入式汇编语句的基本格式如下:
asm(“汇编语句”
: 输出寄存器
: 输入寄存器
: 会被修改的寄存器);
除第1行以外,后面带冒号的行若不使用就都可以省略。其中,”asm”是内联汇编语句的关键词;”汇编语句”是你写汇编指令的地方。
”输出寄存器”表示当这段嵌入式汇编执行完之后,哪些寄存器用来存放输出数据,这些寄存器会分别对应一C语言表达式值或一个内存地址。
”输入寄存器”表示在开始执行汇编代码时,这里指定的一些寄存器中应存放的输入值,它们也分别对应着一C变量或常数值。
”会被修改的寄存器”表示你已对其中列出的寄存器中的值进行了改动,gcc编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,gcc需要重新加载这些寄存器。因此我们需要把那些没有在输出/输入寄存器部分列出,但是在汇编语句中明确使用到或隐含使用到的寄存器名列在这部分。
下面列出了kernel/traps.c文件中第22行开始的一段代码作为例子来进行说明:
#define get_seg_byte(seg,addr) \
({ \
register char __res; \ //定义了一个寄存器变量__res
__asm__("push %%fs; \ //首先保存了fs寄存器原值(段选择符)
mov %%ax,%%fs; \ //然后用seg设置fs
movb %%fs:%2,%%al; \ //取seg:addr处1字节内容到al寄存器中
pop %%fs" \ //恢复fs寄存器原内容
:"=a" (__res) \ //输出寄存器列表
:"0" (seg),"m" (*(addr))); \ //输入寄存器列表
__res;})
这段10行代码定义了一个嵌入式汇编语言宏函数。通常使用汇编语句最方便的方式是把它们放在一个宏内。用圆括号括住的组合语句(花括号中的语句):”({})”可以作为表达式使用,其中最后一行上的变量__res(第10行)是该表达式的输出值,见下一节说明。
因为宏语句需要在一行上,因此这里使用反斜杠’\’将这些语句连接成一行。这条宏定义将被替换到程序中引用该宏名称的地方。第1行定义了宏名称,也即是宏函数名称get_seg_byte(seg,addr)。第3行定义了一个寄存器变量__res,该变量将被保存在一个寄存器中,以便于快速访问和操作。如果想指定寄存器(例如eax),那么我们可以把该句写成”register char __res asm(“ax”);”,其中’asm’也可以写成’__asm__’。第4行上的’__asm__’表示嵌入汇编语句的开始。从第4行到第7行的4条语句是AT&T格式的汇编语句。另外,为了让gcc编译产生的汇编语言程序中寄存器名称前有一个百分号”%”,在嵌入式汇编语句寄存器名称前就必须写上两个百分号”%%”。
第8行即是输出寄存器,这句的含义是在这段代码运行结束后将eax所代表的寄存器的值放入__res变量中,作为本函数的输出值。”=a”中的”a”称为加载代码,”=”表示这是输出寄存器,并且其中的值将被输出值替代。
第9行表示在这段代码开始运行时,将seg放到eax寄存器中,”0”表示使用与上面同个位置的输出相同的寄存器。而(*(addr))表示一个内存偏移地址值。为了在上面汇编语句中使用该地址值,嵌入汇编程序规定把输出和输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以”%0”开始,分别记为%0、%1、…、%9。因此,输出寄存器的编号是%0(这里只有一个输出寄存器),输入寄存器前一部分(“0”(seg))的编号是%1,而后部分的编号是%2。上面第6行上的%2即代表(*(addr))这个内存偏移量。
该宏函数的功能是从指定段和偏移值的内存地址处取一个字节。下面在看一个例子:
asm(“cld\n\t”
“rep\n\t”
“stol”
: /*没有输出寄存器*/
: “c”(count-1), “a”(fill_value), “D”(dest)
: “%ecx”, “edi%”);
第4行说明这段嵌入汇编程序没有用到输出寄存器。第5行的含义是:将count-1的值加载到ecx寄存器中(加载代码是”c”),fill_value加载到eax中,dest放到edi中。表3-4中是一些你可能会用到的寄存器加载代码及其具体的含义:
下面的例子不是让你自己指定哪个变量使用哪个寄存器,而是让gcc为你选择。
asm(“leal (%1,%1,4), %0”
: “=r”(y)
: “0”(x));
“leal (r1, r2, 4), r3”语句表示:(r1+r2*4) -> r3。如果gcc将r指定为eax的话,那么上面汇编语句的含义为:
“leal (eax,eax,4), eax”
注意:在执行代码时,如果不希望汇编语句被gcc优化而作修改,就需要在asm符号后面添加关键词volatile,下面列出了两种声明方式:
asm volatile (…);
__asm__ __volatile__ (…); //建议使用这种方式,兼容性更好
关键词volatile也可以放在函数名前来修饰函数,用来通知gcc编译器该函数不会返回,即该函数不会返回到调用者代码中(例如do_exit()函数)。
下面看一个比较长的例子,该代码是从include/string.h文件中摘取的,是strncmp()字符串比较函数的一种实现。其中每行中的”\n\t”是用于gcc预处理程序输出列表好看而设置的。
//字符串1和字符串2的前count个字符进行比较
//参数: cs:字符串1, ct:字符串2, count:比较的字符数
//%0:eax(__res)返回值, %1:edi(cs)串1指针, %2:esi(ct)串2指针, %3:ecx(count)
//返回:如果串1>串2,则返回1; 串1==串2,则返回0; 串1<串2,则返回-1
extern inline int strncmp(const char * cs,const char * ct,int count)
{
register int __res __asm__("ax"); //__res是寄存器变量,该变量保存在寄存器eax中
__asm__("cld\n" //清方向位
"1:\tdecl %3\n\t" //count--
"js 2f\n\t" //如果count<0,则向前跳转到标号2
"lodsb\n\t" //取串2的字符ds:[esi]->al,并且esi++
"scasb\n\t" //比较al与串1的字符es:[edi],并且edi++
"jne 3f\n\t" //如果不相等,则向前跳转到标号3
"testb %%al,%%al\n\t" //该字符是NULL字符吗?
"jne 1b\n" //不是,则向后跳转到标号1,继续比较
"2:\txorl %%eax,%%eax\n\t" //是NULL字符,则eax清零(返回值)
"jmp 4f\n" //向前跳转到标号4,结束
"3:\tmovl $1,%%eax\n\t" //eax中置1
"jl 4f\n\t" //如果前面比较中,串2字符<串1字符,则返回1,结束
"negl %%eax\n" //否则eax=-eax,返回负值,结束
"4:"
:"=a" (__res):"D" (cs),"S" (ct),"c" (count):"si","di","cx");
return __res; //返回比较结果
}
3、圆括号中的组合语句
花括号对”{…}”用于把变量声明和语句组合成一个复合语句(组合语句)或一个语句块,这样在语义上这些语句就等同于一条语句,组合语句的右花括号后面不需要使用分号。
圆括号中的组合语句,即形如”({…})”的语句,可以在GNU C中用作一个表达式使用。这样就可以在表达式中使用loop、switch语句和局部变量,因此这种形式的语句通常称为语句表达式。语句表达式具有如下示例的形式:
({int y = foo(); int z;
if(y > 0) z = y;
else z = -y;
3 + z;
})
其中组合语句中最后一条语句必须是后面跟随一个分号的表达式。这个表达式(“3+z”)的值即用作整个圆括号括住语句的值。如果最后一条语句不是表达式,那么整个语句表达式就具有void类型,因此没有值。这种表达式中语句声明的任何局部变量都会在整个语句结束后失效。
4、寄存器变量
GNU 对C语言的另一个扩充是允许我们把一些变量值放到CPU寄存器中,即所谓的寄存器变量。寄存器变量可以分为2种:全局寄存器变量和局部寄存器变量。
在GNU C程序中我们可以在函数中用如下形式定义一个局部寄存器变量:
register int res __asm__(“ax”);
这里ax是变量res所希望使用的寄存器。
5、内联函数
在程序中,通过把一个函数声明为内联(inline)函数,就可以让gcc把函数的代码集成到调用该函数的代码中去。这样处理可以去掉函数调用时进入/退出时间开销,从而肯定能够加快执行速度。因此把一个函数声明为内联函数的主要目的就是能够尽量快速的执行函数体。
内联函数嵌入调用者代码中的操作是一种优化操作,因此只有进行优化编译时才会执行代码嵌入处理。若编译过程中没有使用优化选项”- 0”,那么内联函数的代码就不会被真正的嵌入到调用者代码中,而是只作为普通函数调用来处理。
把一个函数声明为内联函数的方法是在函数声明中使用关键词’inline’,例如内核文件fs/inode.c中的如下函数:
inline int inc(int *a)
{
(*a)++;
}
函数中的某些语句用法可能会使得内联函数的替换操作无法正常进行,或者不适合进行替换操作。例如使用了可变参数、内存分配函数malloca()、可变长度数据类型变量、非局部goto语句、以及递归函数。编译时可以使用选项-Winline让gcc对标志成inline但不能被替换的函数给出警告信息以及不能替换的原因。
ISO标准C99的内联函数语义定义等同于使用组合关键词inline和static的定义,即省略了关键词”static”。若在程序中需要使用C99标准的语义,那么就需要使用编译选项-std=gnu99。不过为了兼容起见,在这种情况下还是最好使用inline和static组合。
关键词inline和extern组合在一起的作用几乎类同一个宏定义。使用这种组合方式就是把带有组合关键词的一个函数定义放在.h头文件中,并且把不含关键词的另一个相同函数定义放在一个库文件中。此时头文件中的定义会让绝大数对该函数的调用被替换嵌入。如果还有没有被替换的对该函数的调用,那么就会使用程序文件中或库中的拷贝。Linux 0.1.x内核源代码中文件include/string.h、lib/strings.c就是这种使用方式的一个例子。例如,string.h中定义了如下函数:
//将字符串(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; //返回目的字符串指针
}
而在内核函数库目录中,lib/strings.c文件把关键词inline和extern都定义为空,如下所示。因此实际上就在内核函数库中又包含了string.h文件所有这类函数的一个拷贝,即又对这些函数重新定义了一次,并且”消除”了两个关键词的作用。
#define extern //定义为空
#define inline //定义为空
#define __LIBRARY__
#include <string.h>
此时库函数中重新定义的上述strcpy()函数变成如下形式:
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; //返回目的字符串指针
}
排版更好的内容见我博客的地址:http://www.only2fire.com/archives/79.html
注:转载请注明出处,谢谢!^_^