Chinaunix首页 | 论坛 | 博客
  • 博客访问: 65739
  • 博文数量: 6
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 81
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-07 02:59
文章存档

2016年(6)

我的朋友

分类: LINUX

2016-05-18 14:24:37

[本博客连载并且会持续修正,转载请注明出处:http://blog.chinaunix.net/uid-26583089-id-5729097.html]

    Linux内核中的C语言
    计算机高级语言本质上就是通过一套规则将一段接近人类语言的内容,翻译成计算机机能理解的二进制。这套规则正是实现在编译器里,比如GNU C通过gcc(linux)编译,ANSI C通过vs(windows)编译。GNU C从语言层面相对于ANSI C做了一些扩展,所在一段跨平台的程序可以通过gcc编译,也不一定能用vs编译。
    GNU C增加了inline、const、long long int等,不一一列举。
    gcc也一直在扩展,所以linux内核内核编译不过时,可能需要更新gcc版本。
    语言使用技巧:do {} while(0)等,不一一列举。
    一个重要的头文件:include/linux/list.h(一定要看懂!

    关于list.h中list_entry()宏函数的解释:
    #define list_entry(ptr, type, member) \
        ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

    作用:根据结构成员的地址,求结构的地址。
    参数:ptr-成员地址,type-结构名,member-成员名。
    实例:根据t中list的地址,求t的地址。
    struct test {
        int num;
        struct list_head list;
    };

    struct test t, *tp;
    tp = list_entry(&t.list, struct test, list);  // 这里只是举例,说明可以通过成员地址求结构地址,并不符合应用场景

    原理:
    将list_entry(&t.list, struct test, list)展开得到:
    ((struct test*)((char *)(&t.list)-(unsigned long)(&((struct test*)0)->list)))
    首先看&((struct test*)0)->list,这是假设t的地址是0时,求list成员的地址,其实就是求list相对于整个结构的offset。有意思的是这样用空指针,不会出现段错误吗?只取地址,不取内容是不会段错误的,tp->list其实等价于*(&tp->list),如果tp=NULL,到*操作的时候,才会发生段错误(等学习第二章的时候,就会知道段错误到底是怎么回事),而这里加了&,就不会执行*操作。
    然后,用&t.list真正的地址,往前推offset,就是t的地址了。

    Linux内核中的汇编语言
    汇编语言使用场合:
    有些直接与硬件打交道的操作,C里没有对应的语言成分,比如inb、outb等;
    有些会被频繁调用的底层函数对效率要求极高;
    有些程序对体积有要求,比如引导程序,不能超过一个扇区的大小。

    类似不用ANSI C,而用GUN C,Linux内核不用Intel格式汇编,而用AT&T格式汇编。
    AT&T与Intel格式汇编关键区别(一定要看一下);
    Linux内核中,有一些纯汇编.s文件(.S表示预处理之前的汇编文件),也有嵌入到C中的汇编代码片段。

    嵌入C代码中的386汇编语言程序段(与下面的实例对照着看):
    指令部:输出部:输入部:损坏部,指令部必须有,其它部分可根据情况省略。
    约束条件:   
    m -- 内存单元
    r -- 任何寄存器
    q -- eax, ebx, ecx, edx之一
    a, b, c, d -- 分别表示eax, ebx, ecx, edx
    S, D -- 分别表示esi, edi
    。。。

    实例:从from拷贝n大小的数据到to。
    static inline void *__memcpy(void *to, const void *from, size_t n)
    {
        int d0, d1, d2;
        __asm__ __volatile__(
            "rep; movsl\n\t"
            "testb $2, %b4\n\t"
            "je 1f\n\t"
            "movsw\n"
            "1:\ttestb $1, %b4\n\t"
            "je 2f\n\t"
            "2:"
            : "=&c"(d0), "=&D"(d1), "=&S"(d2)
            : "0"(n/4), "q"(n), "1"((long)to), "2"((long)from)
            : "memory"
        );
        return (to);
    }

    ① 将输出部、输入部,将每个逗号隔开的部分,从0开始依次标号
            0          1          2
    : "=&c"(d0), "=&D"(d1), "=&S"(d2)
          3         4             5              6
    : "0"(n/4), "q"(n), "1"((long)to), "2"((long)from)

    ② 输入部、输出部解释
    输入部相当于初始化。"0"(n/4)表示d0=n/4;"q"(n)表示从eax,ebx,ecx,edx选择一个,并初始化为n,指令部中的%b4就是使用这个寄存器;"1"((long)to)表示将参数传来的to初始化到d1;"2"((long)from)表示将参数传来的from初始化到d2。
    输出部表示会被赋值的变量,提示编译器根据情况,添加一些恢复的代码,以及表示一些结合关系。"=&c"(d0)表示d0变量与ecx结合,"=&D"(d1)表示d1变量与edi结合,"=&S"(d2)表示d2与esi结合。结合上面说明的输入部,其实指令部执行之前,就已经做好了这些准备:ecx=n/4,edi=to,esi=from。

    ③ 指令部解释
    "rep; movsl\n\t",由于ecx、edi、esi已经被初始化,这条指令相当于将from前n/4个int复制到to,那么可能还剩0、1、2或3个字节还没有复制;
    "testb $2, %b4\n\t",%b4表示"q"(n),如果n的第2位为0,那就只可能还剩0或1字节没有复制,向前跳转到第一个1:标号处执行("je 1f\n\t"),f表示froward,否则"movsw",再复制2个字节;
    "1: \ttestb $1, %b4\n\t",执行到这条指令,肯定还剩0或1个字节没有复制,如果n最低位为0,表示复制完了,跳转到2:标号处啥都不执行,否则"movsb\n",再复制1个字节。
    到此,from到to的拷贝就完成了,其中指令中出现的\n、\t代表换行、tab。

    学习Linux内核之前,最好有过开发经验,要不然很难理解它的一些设计思想。Linux内核代码大部分是C写的,所以要非常熟练C语言,汇编代码不多,可以不熟练,但一些关键的地方都是用汇编写的,一定要保证能看得懂。如果满足这些条件,并且已经对“预备知识”有比较不错的理解了,就可以开始学习Linux内核啦
阅读(2712) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~