本节将会介绍一些常用的系统内核库函数,这些库函数都是作者参考linux系统内核的库函数实现的。根据多方面考虑,暂时就没把这些库函数写的那么复杂。有可能功能不完善、性能不够或者有考虑不周的地方,都将会在以后的内核开发过程中一步一步的完善。现在把他们拿来让读者们学习学习这些内核库函数的基本原理,还是非常适合的。不论是从难易程度、代码复杂度、还是实现原理,都相对比较简单。那还等什么!让我们开始吧~~

container_of

    #define container_of(ptr,type,member)                            \
    ({                                            \
        typeof(((type *)0)->member) * p = (ptr);                    \
        (type *)((unsigned long)p - (unsigned long)&(((type *)0)->member));        \
    })  

相信看过linux内核的读者们对container_of的作用并不陌生,它可以根据某个结构体内的成员变量的地址,准确的找到该结构体的首地址。这就相当于一个反向寻找上层结构体的过程,使用成员变量的地址偏移,反推出上层结构的地址。

可能这个宏看起来非常的复杂,但是大家不用担心,我们从他的参数开始,一层一层的剥去他的外衣,原理自然就显现出来。

先从他的参数讲起:

  • ptr:这个变量是一个指针,它指向一实例化后的结构体成员的地址。我们正是要根据这个地址找到它所在的结构体的首地址。

  • type:这个变量代表的是一个结构体,这个结构体就是我们要找的结构体。

  • member:这个变量代表结构体中的成员。这个结构体中的成员,与ptr所指的成员是同一个。只不过member是结构体中的成员,而ptr则是经过实例化后的结构体中的成员。

根据这三个参数,我们就能直接分析出,container_of的计算原理。其实就是用type类型的结构体到其member成员的偏移值,再用ptr指向的内存地址减去这个偏移值,得到的地址就是我们要的结构体的首地址啦。

了解了其实现原理,我们再来结合代码,看看他是如何实现的。

typeof(((type *)0)->member) * p = (ptr);   

这句话的作用是创建一个指针变量p,用来承载ptr变量的地址。p的类型是根据结构体成员member的类型决定的,通过typeof来获得member的类型。其中,(type *)0是将地址0强制转换为结构体type类型的指针,然后,使用((type *)0)->member来引用结构体成员member。最后,就是用typeof获得member变量的类型,并定义成该类型的指针变量p,将ptr的值赋值给p。

(type *)((unsigned long)p - (unsigned long)&(((type *)0)->member));  

这句话的关键在 (unsigned long)&(((type *)0)->member)部分,它同上面的语句很相似,也是将地址0强制转换为type类型的结构体指针,然后引用其成员member,并使用“&”符取出member成员的地址。这个地址是相对于结构体首地址到member成员地址的偏移量。使用0地址的妙处也在于此——将结构体的首地址指定在地址0处,那么从首地址0处到member的地址偏移,实际上就是member的地址,省去了减结构体基地址的步骤。在获得了这个地址偏移量以后,使用变量p里面保存的地址值减去这个偏移量,得到的地址就是结构体的首地址,最后,将地址强制转化为我们要求的结构体类型type的指针,这样一切就都搞定啦~。

memcpy

memcpy可以说是我们日常开发中出场率很高的函数之一了,您想过他是怎么实现的吗?一般我们都不会关注他的内部实现,只要会用就行了。但是从现在开始,整个操作系统都是由我们自己动手开发的,已经不存在系统库函数了。所以,这些函数需要我们动手去实现。下面的函数是基于linux2.4.0内核开发的64位版本的memcpy函数。

inline void * memcpy(void *From,void * To,long Num)
{
    int d0,d1,d2;
    __asm__ __volatile__    (    "cld    \n\t"
                    "rep    \n\t"
                    "movsq    \n\t"
                    "testb    $4,%b4    \n\t"
                    "je    1f    \n\t"
                    "movsl    \n\t"
                    "1:\ttestb    $2,%b4    \n\t"
                    "je    2f    \n\t"
                    "movsw    \n\t"
                    "2:\ttestb    $1,%b4    \n\t"
                    "je    3f    \n\t"
                    "movsb    \n\t"
                    "3:    \n\t"
                    :"=&c"(d0),"=&D"(d1),"=&S"(d2)
                    :"0"(Num/8),"q"(Num),"1"(To),"2"(From)
                    :"memory"
                );
    return To;
}  

考验我们汇编功底的时候到了,你准备好了吗!

这个函数的功能从字面意思就能了解到,是拷贝内存。从From地址到To地址,长度为Num个字节。函数内申请了d0,d1,d2三个参数,分别用来承载输出参数。但是这些参数都是用来在汇编语言内部使用,而临时申请的栈空间,只在memcpy函数内部使用。

首先,让我们先了解memcpy的实现原理,其实他的实现原理很简单,就是使用movs汇编指令将数据转移,并且,借助rep指令进行传输计数。

下面我们来看看movs在Intel技术文档中的解释:

Moves the byte, word, or doubleword specified with the second operand (source operand) to the location specified with the first operand (destination operand). Both the source and destination operands are located in memory. The address of the source operand is read from the DS:ESI or the DS:SI registers (depending on the address-size attribute of the instruction, 32 or 16, respectively). The address of the destination operand is read from the ES:EDI or the ES:DI registers (again depending on the address-size attribute of the instruction).

大概意思:移动拷贝一个字节、字、双字,从指定的源地址到目的地址。其中,源和目的地址都是内存。源操作数的地址从DS:ESI或DS:SI寄存器(根据数据带宽32或16)读入。目标操作数的地址从ES:EDI或ES:DI寄存器(与源操作数寄存器要求一致)。

After the move operation, the (E)SI and (E)DI registers are incremented or decremented automatically according to the setting of the DF flag in the EFLAGS register. (If the DF flag is 0, the (E)SI and (E)DI register are incre-mented; if the DF flag is 1, the (E)SI and (E)DI registers are decremented.) The registers are incremented or decremented by 1 for byte operations, by 2 for word operations, or by 4 for doubleword operations.

大概意思:当移动拷贝以后,(E)SI和(E)DI寄存器会自动增长或者减少,根据EFLAGS寄存器的DF标志位。如果DF标志位为0,(E)SI和(E)DI寄存器,将会自动增加,如果DF标志位为1,那么(E)SI和(E)DI寄存器,将会自动减少。寄存器的增减或者减少会根据操作的字节数执行,1字节,1字,双字。

指令rep在Intel技术文档中的部分解释:

Repeats a string instruction the number of times specified in the count register or until the indicated condition of the ZF flag is no longer met.The REP prefixes apply only to one string instruction at a time. To repeat a block of instructions, use the LOOP instruction or another looping construct. All of these repeat prefixes cause the associated instruction to be repeated until the count in register is decremented to 0.

大概意思:重复执行一串指令若干次,根据指定计数寄存器决定重复次数,或者直到指定条件置位ZF标志位与否。rep指令一次只作用于一条指令。对于想重复一段指令,可以使用loop指令或者其他循环指令。所有这些重复指令功能,直到指定的计数寄存器归零为止。

以上这些解释,就是memcpy的基本原理。下面讲解我们这个内嵌汇编语句的设计思想。由于我们不知道memcpy需要拷贝的数据量是多少,也就是说不能确定参数Num的对其方式,是以字节、字、双字、四字?而且,每一种对齐方式导致的movs指令执行的次数也不一样,并且直接影响执行效率。所以,我们使用以四字(8字节,64位)传输为主,以要拷贝的数据总长度除以8,算出可以传输8字节的数据量。将这部分以movsq的方式将数据复制到目标地址处,然后将参数Num的低字节位(在程序中:%b4)与4、2、1进行与操作,如果与操作后的结果为1,那么进行相应的movsl、movsw、movsb操作,将剩余的输出复制到目的地址上。

在细节上,输出部分使用了“&”符,是因为在输入部分有寄存器约束缩写“q”的存在,导致将有寄存器分配方面的限制。cld指令用于清除DF标志位,使(E)SI和(E)D寄存器在rep指令的作用下自增长。对于损坏部分的“memory”,声明这个内嵌汇编代码段里有修改内存的操作。最后返回To的地址,这个地址只在内嵌汇编语句的输入部分出现过,所以,整个汇编执行的过程不会修改该地址。

memset

有了前面的基础,我想大家理解剩下的内容应该会更加有信心了吧。我们继续看memset函数的代码:

inline void * memset(void * Address,unsigned char C,long Count)
{
    int d0,d1;
    __asm__    __volatile__    (    "cld    \n\t"
                    "rep    \n\t"
                    "stosq    \n\t"
                    "testb    $4,%b3    \n\t"
                    "je    1f    \n\t"
                    "stosl    \n\t"
                    "1:\ttestb    $2,%b3    \n\t"
                    "je    2f    \n\t"
                    "stosw    \n\t"
                    "2:\ttestb    $1,%b3    \n\t"
                    "je    3f    \n\t"
                    "stosb    \n\t"
                    "3:    \n\t"
                    :"=&c"(d0),"=&D"(d1) 
                    :"a"(C),"q"(Count),"0"(Count/8),"1"(Address)    
                    :"memory"                
                );
    return Address;
} 

在memcpy函数里面涉及的内容在memset函数里也有出现,像:rep指令,cld指令,test指令,他们的意思与上文提到的基本相同,相信读者应该不会太过陌生,这里就不再解释啦。我们重点来看一下stosb指令。

指令stosb在Intel技术文档中的解释:

In non-64-bit and default 64-bit mode; stores a byte, word, or doubleword from the AL, AX, or EAX register (respectively) into the destination operand. The destination operand is a memory location, the address of which is read from either the ES:EDI or ES:DI register (depending on the address-size attribute of the instruction and the mode of operation).

大概意思:不管是否是在64位模式下,都会从AL、AX或EAX寄存器保存一个字节、一个字或一个双字到目的地址。这个目的地址是从ES:(E)DI或ES:DI寄存器中读取出来的。

Here also ES:(E)DI is assumed to be the destination operand and AL, AX, or EAX is assumed to be the source operand. The size of the destination and source operands is selected by the mnemonic: STOSB (byte read from register AL), STOSW (word from AX), STOSD (doubleword from EAX).
STOSQ (and its explicit operands variant) store a quadword from the RAX register into the destination addressed by RDI or EDI

大概意思:这里,ES:(E)DI被用来指定目标操作;AL、AX或EAX被用来指定源操作。源和目的操作数的大小根据指令而定,stosb(从AL寄存器读一个字节),stosw(从AX寄存器读一个字),stosd(从EAX寄存器读双字);stosq保存四字从RAX寄存器,到RDI或EDI所指的目标地址中。

注意:这里gcc使用stosl代表双字操作,Intel使用stosd代表双字操作,不是笔误。

相信memset的原理已经很明显了,就是使用stos(b|w|l|q)指令结合rep重复指令,将(R|E)AX寄存器的内容保存到(R|E)DI寄存器所指的地址中。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充