开会迟到了,领导脸上有点绿,问:几点了

我说:3900点

领导:我问你什么时候

我:收盘的时候

领导怒了:出去!

我也急:长生了,全跌停,出。。出。。出不去”

昨天笨叔点滴7里给小伙伴留了小小的疑问?为啥子小明的程序像打了“真”的狂犬疫苗一样乱跳呢?有的朋友说,它没找到真正的栈,看下面这个图。

640.jpg

小明以为通过get_current_pt_regs()函数可以获取第一个进程init_task的栈,可是init_task这个变量,小明同学只是定义成全局的一个变量,它是存在data区的,但是小明天真的以为这个init_task分配了8KB的空间,实际上并没有,仅仅是初始化了一个struct task_struct结构体而已,它的实际大小也只有sizeof(task_struct)。所以通get_current_pt_regs()函数去找栈,那必定会改写了其他数据区的内容哟。

那在Linux内核中是怎么做的? (下面是内核的做法,当然现在内核是存放的thread_info的数据结构,但原理是类似的)

  1. 在vmlinux.lds的链接脚本中data段里预留了8KB的空间给第一个进程用作内核栈。

640.png

  1. 然后定义一个union,并且把 init_task这个变量 安装到这个".data..init_task"段里。

640 (1).png

640 (2).png

所以,就好比上面那张A股图,你以为“底”是在这里,其实真正的“底”是在那边。所以C语言时刻提醒我们需要关注内存情况,思考每个变量它究竟存在内存的什么地方,处处都是“雷”,和A股一样,很容易踩雷,那天就要被ST了。

今天我们和大家分享一下GNU C语言的一些扩展。

01 GCC 扩展

GCC的C编译器除了支持ANSI C标准之外,还对C语言进行了很多的扩充。这些扩充对代码优化、目标代码布局以及安全检查等方面提供了很强的支持,因此支持GNU扩展的C语言称为GNU C语言。Linux内核采用GCC编译器,所以Linux内核的代码自然使用了很多GCC的新扩充特性。本章介绍一些GCC的C语言扩充的新特性,希望读者在学习Linux内核的时候需要特别留意和关注。

1) 语句表达式

在GNU C语言中,括号里的复合语句可以看作是一个表达式,称为语句表达式。在一个语句表达式里,可以使用循环、跳转和局部变量等。这个特性通常用在宏定义中,可以让宏定义变的更安全,如比较两个值的大小。

define max(a,b) ((a) >(b) ? (a) : (b))

上述代码会安全问题,a和b有可能会计算两次,比如a传入i++,b传入j++。在GNU C中,如果知道a和b的类型,可以这样写这个宏。

define maxint(a,b) \

({int _a = (a), _b = (b); _a > _b ? _a :_b; })

如果你不知道a和b的类型,还可以使用typeof类转换宏。

<include/linux/kernel.h>

define min(x, y) ({ \

 typeof(x) _min1 = (x);         \

 typeof(y) _min2 = (y);         \

 (void) (&_min1 == &_min2);     \

 _min1 < _min2 ? _min1 : _min2; })


typeof也是GNU C语言的一个扩充用法,可以用来构造新的类型,通常和语句表达式一起使用。下面是一些例子。

typeof (*x) y;

typeof (*x) z[4];

typeof (typeof (char *)[4])m;

第一句是声明y是x指针指向的类型。第二句声明z是一个数组,其中数组的类型是x指针指向的类型。第三句声明m是一个指针数组,和“char *m[4]”声明是一样的。

2) 零长数组

GNU C允许使用变长数组,在定义数据结构的时候非常有用。

<mm/percpu.c>

struct pcpu_chunk {

 struct list_head   list;       /* linked to pcpu_slot lists */

 unsigned long      populated[];    /* populated bitmap */

};

数据结构最后一个元素定义为零长度数组,不占结构体空间。这样,我们可以根据对象大小动态的去分配结构的大小。

struct line {

int length;

char contents[0];

};

struct line *thisline =malloc(sizeof(struct line) + this_length);

thisline->length =this_length;

如上述例子,structline数据结构定义了一个intlength变量和一个变长数组contents[0],这个struct line数据结构的大小只包含int类型的大小,不包含contents的大小,也就是sizeof (struct line) = sizeof(int)。创建结构体对象时,可根据实际的需要指定这个可变长数组的长度,并分配相应的空间,如上述实例代码分配了this_length个字节的内存,并且可以通过contents[index]来访问第index个地址的数据。

3) case范围

GNU C支持指定一个case的范围作为一个标签,如:

case low ... high:

case 'A' ... 'Z':

这里low到high表示一个区间范围,另外在ASCII字符代码也非常有用,下面是Linux内核中的代码例子。

<arch/x86/platform/uv/tlb_uv.c>

static int local_atoi(constchar *name)

{

 int val = 0;



 for (;; name++) {

     switch (*name) {

     case '0' ... '9':

         val = 10*val+(*name-'0');

         break;

     default:

         return val;

     }

 }

}

另外还可以用整形数来表示范围,但是这里需要注意在“...”两边需要有空格,否则编译出错。

<drivers/usb/gadget/udc/at91_udc.c>

static intat91sam9261_udc_init(struct at91_udc *udc)

{

 for (i = 0; i < NUM_ENDPOINTS; i++) {

     ep = &udc->ep[i];



     switch (i) {

     case 0:

         ep->maxpacket = 8;

         break;

     case 1 ... 3:

         ep->maxpacket = 64;

         break;

     case 4 ... 5:

         ep->maxpacket = 256;

         break;

     }

 }


}

4) 标号元素

标准C语言要求数组或结构体初始化值必须以固定顺序出现,但在GNU C语言中,可以通过指定索引或结构体成员名来初始化,不必按照原来的固定顺序进行初始化。

结构体成员的初始化在Linux内核中经常使用,如在设备驱动中初始化file_operations数据结构,下面是Linux内核中的一个代码例子。

<drivers/char/mem.c>

static const structfile_operations zero_fops = {

 .llseek     = zero_lseek,

 .read       =new_sync_read,

 .write      = write_zero,

 .read_iter  =read_iter_zero,

 .aio_write  =aio_write_zero,

 .mmap       = mmap_zero,

};

如上述代码中的zero_fops的成员llseek初始化为zero_lseek函数,read成员初始化为new_sync_read函数,依次类推。当file_operations数据结构的定义发生变化时,这种初始化方法依然能保证已知元素的正确性,对于未初始化成员的值为0或者NULL。

未完待续。。。


奔跑吧Linux社区
4 声望4 粉丝

奔跑吧Linux社区,为广大小伙伴布道Linux开源!