kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

linux内核调试转储工具kdump crash

http://www.ibm.com/developerworks/cn/linux/l-cn-kdump4/index.html

1
2
3
4
5
$ crash vmlinux vmcore 
crash> bt
crash> dis -l ffffffff80081000
crash> gdb x/8ub ffffffff90091000
......

如果是未完成文件可以尝试以最小方式调试

1
2
$ crash --minimal vmlinux vmcore
crash> log
1
2
3
4
5
6
7
8
9
10
11
crash_H_args_xbt> mod -S
 MODULE   NAME         SIZE  OBJECT FILE
c8019000  soundcore   2788  /lib/modules/2.2.5-15/misc/soundcore.o
。。。
crash_H_args_xbt> mod -s soundcore
 MODULE   NAME         SIZE  OBJECT FILE
c8019000  soundcore   2788  /lib/modules/2.2.5-15/misc/soundcore.o
crash_H_args_xbt> mod -d soundcore
crash_H_args_xbt> mod -s soundcore /tmp/soundcore.o
 MODULE   NAME         SIZE  OBJECT FILE
c8019000  soundcore   2788  /tmp/soundcore.o

1、kdump介绍与设置

1)介绍:

Kdump 是一种基于 kexec 的内存转储工具,目前它已经被内核主线接收,成为了内核的一部分,它也由此获得了绝大多数 Linux 发行版的支持。与传统的内存转储机制不同不同,基于 Kdump 的系统工作的时候需要两个内核,一个称为系统内核,即系统正常工作时运行的内核;另外一个称为捕获内核,即正常内核崩溃时,用来进行内存转储的内核。

安装crash,kexec-tools

2)设置

查看/boot/grub/grub.conf文件中kernel一行最后是否有crashkernel=128M@64M,如果没有,添加上去,重启
如何设定 crashkernel 参数
在 kdump 的配置中,往往困惑于 crashkernel 的设置。“crashkernel=X@Y”,X 应该多大? Y 又应该设在哪里呢?实际我们 可以完全省略“@Y”这一部分,这样,kernel 会为我们自动选择一个起始地址。而对于 X 的大小,般对 i386/x86_64 的系统, 设为 128M 即可;对于 powerpc 的系统,则要设为 256M。rhel6 引入的“auto”已经要被放弃了,代之以原来就有的如下语法:

1
2
3
4
5
6
crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset] 
		  range=start-[end] 
		  'start' is inclusive and 'end' is exclusive. 

		  For example: 
		  crashkernel=512M-2G:64M,2G-:128M

如何判断捕获内核是否加载
可通过查看 /sys/kernel/kexec_crash_loaded 的值。“1”为已经加载,“0”为还未加载。
缩小 crashkernel
可以通过向 /sys/kernel/kexec_crash_size 中输入一个比其原值小的数来缩小甚至完全释放 crashkernel。

3)测试kdump是否可用

执行

1
2
echo 1 > /proc/sys/kernel/sysrq
echo c > /proc/sysrq-trigger

经过两次自动重启后,查看/var/crash/目录下是否有vmcore文件生成,如果有表示kdump可用

2、生成带调试信息的vmlinux文件

1)

centos: debuginfo.centos.org

2)按顺序安装

kernel-debuginfo-common-2.6.18-194.3.1.el5.i686.rpm和kernel-debuginfo-2.6.18-194.3.1.el5.i686.rpm, 之后会在/usr/lib/debug/lib/modules/2.6.18-194.3.1.el5/下生产vmlinux文件
或在源码里make binrpm-pkg -j8,然后该目录下会生成一个vmlinux
在编译内核之前,需要确认.config中,以下编译选项是否打开:

(1)CONFIG_DEBUG_INFO ,必须打开该选项,否则crash会出现以下错误:
crash no debugging data available
(2)CONFIG_STRICT_DEVMEM,必须打开该选项,否则crash会出现以下错误:
crash: read error: kernel virtual address: c0670680 type: “kernel_config_data”
WARNING: cannot read kernel_config_data
crash: read error: kernel virtual address: c066bb68 type: “cpu_possible_mask”

3、进入vmlinux所在目录,

执行crash /var/crash/2012-03-13-21:05/vmcore vmlinux
mod -S XXX –导入XXX目录下所有符号
log –查看日志文件,找到最后一条,如EIP: [] bshtej_interrupt+0x103f/0x11cb [tej21] SS:ESP 0068:c0768f38
l* bshtej_interrupt+0x103f 出现如下内容

1
2
3
4
5
6
7
8
9
10
11
0xf8ee53f5 is in bshtej_interrupt (/opt/dahdi-linux-complete-2.2.1+2.2.1/linux/drivers/dahdi/tej21/tej21.c:2910).
2904          int c, x;
2905
2906
2907          for(c = 0; c < MAX_CARDS; c++)
2908          {
2909              if (!cards[c]) break;
2910              for (x=0;x<cards[c]->numspans;x++) {
2911                  if (cards[c]->tspans[x]->sync)
2912                  {
2913

到此可确定死机问题出现在2910行。

4、设置过滤等级:

vmcore文件一般会收集内核崩溃时的各种信息,所以生成时间会较长,文件比较大,如果不需要某些信息的话,可对kdump.conf文件进行配置

1
vim  /etc/kdump.conf

将core_collector makedumpfile -c 这行打开,并加上-d 31,参数说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-c: Compress dump data by each page.
-d: Specify the type of unnecessary page for analysis.
  Dump  | zero    cache   cache   user    free
  Level | page    page    private data    page
  -------+---------------------------------------
  0  |
  1  |    X
  2  |        X
  4  |        X   X
  8  |                X
  16  |                   X
  31  |   X   X   X   X   X

##### 5、根据Oops值大致判断错误:
Oops的错误代码根据错误的原因会有不同的定义如果发现自己遇到的Oops和下面无法对应的话,最好去内核代码里查找:
  • error_code:
  • bit 0 == 0 means no page found, 1 means protection fault
  • bit 1 == 0 means read, 1 means write
  • bit 2 == 0 means kernel, 1 means user-mode
  • bit 3 == 0 means data, 1 means instruction ```

GCC内嵌汇编

内核代码绝大部分使用C语言编写,只有一小部分使用汇编语言编写,例如与特定体系结构相关的代码和对性能影响很大的代码。GCC提供了内嵌汇编的功能,可以在C代码中直接内嵌汇编语言语句,大大方便了程序设计。

一、基本内嵌汇编

GCC提供了很好的内嵌汇编支持,最基本的格式是:
__asm__ __volatile__(汇编语句模板);

1、asm

__asm__是GCC关键字asm的宏定义:
#define __asm__ asm
__asm__或asm用来声明一个内嵌汇编表达式,所以任何一个内嵌汇编表达式都是以它开头的,是必不可少的。

2、汇编语句模板

“汇编语句模板”是一组插入到C程序中的汇编指令(可以是单个指令,也可以是一组指令)。每条指令都应该由双引号括起,或者整组指令应该由双引号括起。每条指令还应该用一个定界符结尾。有效的定界符为换行符(\n)和分号(;)。\n后可以跟一个制表符(\t)作为格式化符号,增加GCC在汇编文件中生成的指令的可读性。

上述原则可以归结为:
①任意两个指令间要么被分号(;)分开,要么被放在两行;
②放在两行的方法既可以通过\n的方法来实现,也可以真正的放在两行;
③可以使用一对或多对双引号,每对双引号里可以放任意多条指令,所有的指令都必须放到双引号中。
在基本内嵌汇编中,“汇编语句模板”的书写的格式和你直接在汇编文件中使用汇编语言编程没有什么不同,你可以在其中定义标号(Label),定义对齐(.align n),定义段(.section name)。例如:

1
2
3
4
5
6
7
__asm__(".align 2\n\t"
"movl %eax, %ebx\n\t"
"test %ebx, %ecx\n\t"
"jne error\n\t"
"sti\n\t"
"error: popl %edi\n\t"
"subl %ecx, %ebx");

建议大家都使用这种格式来写内嵌汇编代码。

3、volatile

__volatile__是GCC关键字volatile的宏定义:
#define __volatile__ volatile
__volatile__volatile是可选的。如果不想让GCC的优化改动你的内嵌汇编代码,你最好在前面都加上__volatile__

二、带C语言表达式的内嵌汇编

在内嵌汇编中,可以将C语言表达式指定为汇编指令的操作数,而且不用去管如何将C语言表达式的值读入哪个寄存器,以及如何将计算结果写回C变量,你只要告诉程序中C语言表达式与汇编指令操作数之间的对应关系即可,GCC会自动插入代码完成必要的操作。

通常嵌入到C代码中的汇编语句很难做到与其它部分没有任何关系,因此更多时候需要用到扩展的内嵌汇编格式:
__asm__ __volatile__(汇编语句模板 : 输出部分 : 输入部分 : 破坏描述部分);

内嵌汇编表达式包含4个部分,各部分由“:”分隔。这4个部分都不是必须的,任何一个部分都可以为空,其规则为:
①如果“破坏描述部分”为空,则其前面的“:”必须省略。比如:
__asm__("mov %%eax, %%ebx" : :);。 ②如果“汇编语句模板”为空,则“输出部分”,“输入部分”以及“破坏描述部分”可以不为空,也可以为空。比如:
__asm__("" : : : "memory");。 ③如果“输出部分”,“输入部分”以及“破坏描述部分”都为空,“输出部分”和“输入部分”之前的“:”既可以省略,也可以不省略。如果都省略,则此汇编退化为一个基本内嵌汇编,否则,仍然是一个带有C语言表达式的内嵌汇编。
④如果“输入部分”和“破坏描述部分”为空,但“输出部分”不为空,“输入部分”前的“:”既可以省略,也可以不省略。
⑤如果后面的部分不为空,而前面的部分为空,则前面的“:”都必须保留,否则无法说明不为空的部分究竟是第几部分。
⑥如果“破坏描述部分”不为空,而“输出部分”和“输入部分”都为空,则“输出部分”和“输入部分”前的“:”都必须保留。
从上面的规则可以看到另外一个事实,区分一个内嵌汇编是基本格式的还是扩展格式的,其规则在于在“汇编语句模板”后面是否有“:”的存在,如果没有则是基本格式的,否则,就是扩展格式的。
这两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个%,这一点和原生汇编相同;而扩展格式则要求寄存器前必须使用两个%%。比如:
__asm__("mov %%eax, %%ebx" :)

__asm__("mov %eax, %ebx")
都是正确的写法,而
__asm__("mov %eax, %ebx" :)

__asm__("mov %%eax, %%ebx")
都是错误的写法。任何只带一个“%”的标识符都看成是操作数,而不是寄存器。

1、内嵌汇编举例

使用内嵌汇编,要先编写汇编语句模板,然后将C语言表达式与指令的操作数相关联,并告诉GCC对这些操作有哪些约束条件。例如在下面的汇编语句:
__asm__("movl %1, %0" : "=r"(result) : "m"(input));
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数,称为占位符,内嵌汇编靠它们将C语言表达式与指令操作数相对应。指令模板后面用圆括号括起来的是C语言表达式,本例中只有两个:“result”和“input”,他们按照在输出部分和输入部分出现的顺序分别与指令操作数“%0”,“%1”对应;注意对应顺序:第一个C语言表达式对应“%0”;第二个表达式对应“%1”,依次类推。在每个操作数前面有一个用双引号括起来的字符串,字符串的内容是对该操作数的约束或者说要求。“result”前面的约束字符串是“=r”,其中“=”表示“result”在指令中是只写的(输出操作数),“r”表示需要将“result”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后在指令中使用相应寄存器,而不是“result”本身,当然指令执行完后需要将寄存器中的值存入变量“result”,从表面上看好像是指令直接对“result”进行操作,实际上GCC做了隐式处理,这样我们可以少写一些指令。“input”前面的“r”表示该表达式需要先放入某个寄存器,然后在指令中使用该寄存器参加运算。
由此可见,C语言表达式或者变量与寄存器的关系由GCC自动处理,我们只需使用约束字符串指导GCC如何处理即可。
内联汇编的重要性体现在它能够灵活操作,而且可以使其输出通过C变量显示出来。因为它具有这种能力,所以__asm__可以用作汇编指令和包含它的C程序之间的接口。

2、汇编语句模板

◆操作数
C语言表达式可用作内嵌汇编中的汇编指令的操作数。在汇编指令通过对C语言表达式进行操作来执行有意义的作业的情况下,操作数是扩展格式的内嵌汇编的主要特性。
每个操作数都由操作数约束字符串指定,后面跟着用圆括号括起来的C语言表达式,例如:
“constraint”(C expression)
操作数约束的主要功能是确定操作数的寻址方式。

◆占位符
在扩展格式的内嵌汇编的“汇编语句模板”中,操作数由占位符引用。如果总共有n个操作数(包括输入和输出),那么第一个输出操作数的编号为0,逐项递增,总操作数的数目限制在10个(%0、%1、…、%9)。
如果要处理很多输入和输出操作,数字型的占位符很快就会变得混乱。为了使条理清晰,GNU编译器(从版本3.1开始)允许声明替换的名称作为占位符。
替换的名称在“输入部分”和“输出部分”中声明。格式如下:
[name] "constraint"(C expression) 声明name后,使用%[name]的形式替换内嵌汇编代码中相应的数字型占位符。如下面所示:

1
2
3
__asm__("cmoveq %1, %2, %[result]"
: [result] "=r"(result)
: "r"(test), "r"(new), "[result]"(old));

在内嵌汇编中使用占位符表示的操作数,总被视为long型(4个字节) ,但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是高字节。方法是在%和序号之间插入一个字母,“b”代表低字节,“h”代表高字节,例如:%h1。

必须使用占位符的情况: 我们看一看下面这个例子:

1
2
3
__asm__("addl %1, %0"
: "=a"(out)
: "m"(in1), "a"(in2));

①首先,我们看一看上例中的第1个输入操作表达式"m"(in1),它被GCC替换之后,表现为addl address_of_in1, %%eax,in1的地址是什么?编译时才知道。所以我们完全无法直接在指令中去写出in1的地址,这时使用占位符,交给GCC在编译时进行替代,就可以解决这个问题。所以这种情况下,我们必须使用占位符。
②其次,如果上例中的输出操作表达式"=a"(out)改为"=r"(out),那么out究竟会使用哪个寄存器只有到编译时才能通过GCC来决定,既然在我们写代码的时候,我们不知道究竟哪个寄存器被选择,我们也就不能直接在指令中写出寄存器的名称,而只能通过占位符替代来解决。

3、输出部分

“输出部分”用来指定当前内嵌汇编语句的输出。我们看一看这个例子:
__asm__("movl %%cr0, %0" : "=a"(cr0));
这个内嵌汇编语句的输出部分为"=r"(cr0),它是一个“操作表达式”,更具体地在这里叫作“输出操作表达式”,指定了一个输出操作。“输出操作表达式”由两部分组成,这两部分都是必不可少的:
①圆括号括起来的部分是一个C语言表达式,用来保存内嵌汇编的一个输出值,其操作就等于C的赋值表达式cr0 = output_value,因此,圆括号中的输出表达式只能是C的左值表达式。那么右值output_value从何而来呢?
②答案是双引号中的内容,被称作“操作约束”(Operation Constraint),在这个例子中操作约束为"=a",它包含两个约束:等号(=)和字母a,其中等号(=)说明圆括号中左值表达式cr0是Write-Only的,只能够被作为当前内嵌汇编的输出,而不能作为输入。而字母a是寄存器EAX/AX/AL的简写,说明cr0的值要从EAX寄存器中获取,也就是说cr0 = %eax,最终这一点被转化成汇编语句就是movl %eax, address_of_cr0。
另外,需要特别说明的是,很多文档都声明,所有输出操作的操作约束必须包含一个等号(=),但GCC的文档中却很清楚的声明,并非如此。因为等号(=)约束说明当前的表达式是Write-Only的,但另外还有一个符号——加号(+)用来说明当前表达式是Read-Write的,如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是Read-Only的。因为对于输出操作来说,肯定是必须是可写的,而等号(=)和加号(+)都表示可写,只不过加号(+) 同时也表示是可读的。所以对于一个输出操作来说,其操作约束只需要有等号(=)或加号(+)中的任意一个就可以了。二者的区别是:等号(=)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则表示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。但无论是等号(=)约束还是加号(+)约束所约束的操作表达式都只能放在“输出部分”中,而不能被用在“输入部分”中。
在“输出部分”中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。

4、输入部分

“输入部分”的内容用来指定当前内嵌汇编语句的输入。我们看一看这个例子:
__asm__("movl %0, %%db7" : : "a"(cpu->db7));
例中“输入部分”的内容为一个表达式"a"(cpu->db7),被称作“输入操作表达式”,用来表示一个对当前内嵌汇编的输入。
像输出操作表达式一样,一个输入操作表达式也分为两部分:带圆括号的部分(cpu->db7)和带双引号的部分"a"。这两部分对于一个内嵌汇编输入操作表达式来说也是必不可少的。
圆括号中的表达式cpu->db7是一个C语言的表达式,它不必是一个左值表达式,也就是说它不仅可以是放在C赋值操作左边的表达式,还可以是放在C赋值操作右边的表达式。所以它可以是一个变量,一个数字,还可以是一个复杂的表达式。比如上例可以改为:

1
2
3
__asm__("movl %0, %%db7" : : "a"(foo));
__asm__("movl %0, %%db7" : : "a"(0x1000));
__asm__("movl %0, %%db7" : : "a"(x*y/z));

双引号中的部分是约束部分,和输出操作表达式约束不同的是,它不允许指定加号(+)约束和等号(=)约束,也就是说它只能是默认的Read-Only的。约束中必须指定一个寄存器约束,例中的"a"表示当前输入变量cpu->db7要通过寄存器%eax输入到当前内嵌汇编中。
在“输入部分”中可以有多个输入操作表达式,多个操作表达式中间必须用逗号(,)分开。

5、操作约束

前面提到过,在内嵌汇编中的每个操作数都应该由操作数约束字符串描述,后面跟着用圆括号括起来的C语言表达式。操作数约束主要是确定指令中操作数的寻址方式。约束也可以指定:
①是否允许操作数位于寄存器中,以及它可以包括在哪些类型的寄存器中
②操作数是否可以是内存引用,以及在这种情况下使用哪些类型的寻址方式
③操作数是否可以是立即数

约束字符必须与指令对操作数的要求相匹配,否则产生的汇编代码将会有错,在这个例子中:
__asm__("movl %1,%0" : "=r"(result) : "r"(input));
如果将那两个"r",都改为"m"(“m”表示操作数是内存引用)编译后得到的结果是:
movl input, result
很明显这是一条非法指令(mov不允许内存到内存的操作)。

每一个输入和输出操作表达式都必须指定自己的操作约束,下面是在80x86平台上可能使用的操作约束:
◆寄存器约束
当你当前的输入或输出需要借助一个寄存器时,你需要为其指定一个寄存器约束。你可以直接指定一个寄存器的名字,比如:
__asm__("movl %0, %%cr0" : : "eax"(cr0));
也可以指定一个缩写,比如:
__asm__("movl %0, %%cr0" : : "a"(cr0));
如果你指定一个缩写,比如“a”,则GCC将会根据当前操作表达式中C语言表达式的类型决定使用%eax,还是%ax或%al。比如:

1
2
unsigned short shrt;
__asm__("mov %0,%%bx" : : "a"(shrt));

由于变量shrt是16-bit short类型,则编译出来的汇编代码中,会让此变量使用%ax寄存器。
无论是输入还是输出的操作表达式,都可以使用寄存器约束。

◆内存约束
如果一个输入或输出操作表达式的C语言表达式表现为一个内存地址,并且不想借助于任何寄存器,则可以使用内存约束。比如:
__asm__("lidt %0" : "=m"(idt_addr));
使用内存方式进行输入输出时,由于不借助寄存器,所以GCC不会按照你的声明对其作任何的输入输出处理。GCC只会直接拿来用,究竟对这个C语言表达式而言是输入还是输出,完全依赖与你写在“汇编语句模板”中的指令对其操作的指令。
当操作数位于内存中时,任何对它们执行的操作都将在内存位置中直接发生,所以,对于内存约束类型的操作表达式而言,放在“输入部分”还是放在“输出部分”,对编译结果是没有任何影响的,既然对于内存约束类型的操作表达式来说,GCC不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序员的角度而言,为了增强代码的可读性,最好能够把它放在符合实际情况的地方。

◆立即数约束
如果一个输入或输出操作表达式的C语言表达式是一个数字常数,并且不想借助于任何寄存器,则可以使用立即数约束。
由于立即数在C中只能作为右值,所以对于使用立即数约束的操作表达式而言,只能放在“输入部分”。比如:
__asm__("movl %0, %%eax" : : "i"(100));

◆匹配约束
匹配约束符是一位数字:“0”,“1”,…,“9”,表示它约束的C表达式分别与占位符%0,%1,…,%9相对应的C变量匹配。例如使用“0”作为%1的约束字符,那么%0和%1表示同一个C变量。

在某些情况下,一个变量既要充当输入操作数,也要充当输出操作数。可以通过使用匹配约束在内嵌汇编中的“输入部分”指定这种情况。
__asm__("incl %0" : "=a"(var) : "0"(var));
在上面的示例中,寄存器%eax既用作输入变量,也用作输出变量。将输入变量var读取到%eax,执行inc指令后将更新了值的%eax再次存储在var中。这里的"0"指定与第0个输出变量相同的约束。即,它指定var的输出实例只应该存储在%eax中。

该约束可以用于以下情况:
①输入从变量中读取,或者变量被修改后,修改写回到同一变量中
②不需要将输入操作数和输出操作数的实例分开
使用匹配约束最重要的意义在于它们可以导致有效地使用可用寄存器。

i386指令集中许多指令的操作数是读写型的,例如:
addl %1, %0
它先读取%0与%1原来的值然后将两者的值相加,并把结果存回%0,因此操作数%0是读写型操作数。老版本的GCC对这种类型操作数的支持不是很好,它将操作数严格分为输入和输出两种,分别放在输入部分和输出部分,而没有一个单独部分描述读写型操作数。
__asm__("addl %1, %0" : "=r"(result) : "r"(input));
上例使用“r”约束的输出变量,GCC会分配一个寄存器,然后用该寄存器替换占位符,但是在使用该寄存器之前并不将result变量的值先读入寄存器,GCC认为所有输出变量以前的值都没有用处,也就没有必要将其读入寄存器(这可能是因为AT&T汇编源于RISC架构处理器的原故,在RISC处理器中大部分指令的输入输出明显分开,而不像CISC那样一个操作数既做输入又做输出,例如:
add r0, r1, r2
r0和r1是输入,r2是输出,输入和输出分开,不使用输入输出型操作数。这种情况下GCC理所当然认为所有输出变量以前的值都没有用处,也就没有必要先将输出操作数的值读入寄存器r2了)。
上面的内嵌汇编指令不能奏效,因为需要在执行addl之前把result的值入寄存器。因此在GCC中读写型的操作数需要在输入和输出部分分别描述,靠匹配约束符将两者关联到一起。注意仅在输入和输出部分使用相同的C变量,但是不用匹配约束符,例如:
__asm__("addl %2, %0" : "=r"(result) : "r"(result), "m"(input));
产生的代码很可能不对。
看上去上面的代码可以正常工作,因为我们知道%0和%1都和result相关,应该使用同一个寄存器,但是GCC并不去判断%0和%1是否和同一个C语言表达式或变量相关联(这样易于产生与内嵌汇编相应的汇编代码),因此%0和%1使用的寄存器可能不同。
使用匹配约束符后,GCC知道应将对应的操作数放在同一个位置(同一个寄存器或者同一个内存变量)。使用匹配约束字符的代码如下:
__asm__("addl %2,%0" : "=r"(result) : "0"(result), "m"(input));
相应的汇编代码为:

1
2
3
4
5
6
7
8
9
  movl $0, _result
  movl $1, _input
  movl _result, %edx
  movl %edx, %eax
#APP
  addl _input, %eax
#NO_APP
  movl %eax, %edx
  movl %edx, _result

可以看到与result相关的寄存器是%edx,在执行指令addl之前先从%edx将result读入%eax,执行之后需要将结果从%eax读入%edx,最后存入result中。这里我们可以看出GCC处理内嵌汇编中输出操作数的一点点信息:addl并没有使用%edx,可见它不是简单的用result对应的寄存器%edx去替换%0,而是先分配一个寄存器,执行运算,最后才将运算结果存入对应的变量,因此GCC是先看该占位符对应的变量的约束符,发现是一个输出型寄存器变量,就为它分配一个寄存器,此时没有去管对应的C变量,最后GCC知道还要将寄存器的值写回变量,与此同时,它发现该变量与%edx关联,因此先存入%edx,再存入变量。

在新版本的GCC中增加了一个约束字符“+”,它表示操作数是读写型的,GCC知道应将变量值先读入寄存器,然后计算,最后写回变量,而无需在输入部分再去描述该变量。
__asm__("addl %1, %0" : "+r"(result) : "m"(input));
产生的汇编代码如下:

1
2
3
4
5
6
7
8
9
  movl $0,_result
  movl $1,_input
  movl _result,%eax
#APP
  addl _input,%eax
#NO_APP
  movl %eax,_result
L2:
  movl %ebp,%esp

处理的比使用匹配约束符的情况还要好,省去了好几条汇编代码。

◆修饰符 等号(=)和加号(+)用于对输出操作表达式的修饰,一个输出操作表达式要么被等号(=)修饰,要么被加号(+)修饰,二者必居其一。使用等号(=)说明此输出操作表达式是Write-Only的,使用加号(+)说明此输出操作表达式是Read-Write的。它们必须是输出操作表达式约束字符串中的第一个字符。比如:"a=“(var)是非法的,而”+g"(var)则是合法的。
当使用加号(+)的时候,此输出操作表达式等价于使用等号(=)约束再加上一个输入操作表达式。比如:
__asm__("incl %0" : "+a"(var));
等价于
__asm__("incl %0" : "=a"(var) : "0"(var));

像等号(=)和加号(+)修饰符一样,符号(&)也只能用于对输出操作表达式的修饰。
约束符“&”表示输入和输出操作数不能使用相同的寄存器,这样可以避免很多错误。举一个例子,下面代码的作用是将函数foo的返回值存入变量ret中:
__asm__("call foo; movl %%edx, %1" : "=a"(ret) : "r"(bar));
我们知道函数的int型返回值存放在%eax中,但是GCC编译的结果是输入和输出同时使用了寄存器%eax,如下:

1
2
3
4
5
6
  movl bar, %eax
#APP
  call foo
  movl %ebx, %eax
#NO_APP
  movl %eax, ret

结果显然不对,原因是GCC并不知道%eax中的值是我们所要的。避免这种情况的方法是使用“&”修饰符,这样bar就不会再使用%eax寄存器,因为已被ret指定使用。
__asm__("call foo; movl %%edx, %1" : "=&a"(ret) : "r"(bar));

6、破坏描述部分

有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样就难免会破坏原有寄存器的内容。如果希望GCC在编译时能够将这一点考虑进去。那么你就可以在“破坏描述部分”声明这些寄存器或内存。

这种情况一般发生在一个寄存器出现在“汇编语句模板”,但却不是由输入或输出操作表达式所指定的,也不是在一些输入或输出操作表达式使用"r"、"g"约束时由GCC为其选择的,同时此寄存器被“汇编语句模板”中的指令修改,而这个寄存器只是供当前内嵌汇编临时使用的情况。比如:
__asm__("movl %0, %%ebx" : : "a"(foo) : "%ebx");
寄存器%ebx出现在“汇编语句模板”中,并且被movl指令修改,但却未被任何输入或输出操作表达式指定,所以你需要在“破坏描述部分”指定"%ebx",以让GCC知道这一点。

因为你在输入或输出操作表达式所指定的寄存器,或当你为一些输入或输出操作表达式使用"r"、"g"约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚的——它知道这些寄存器是被修改的,你根本不需要在“破坏描述部分”再声明它们。但除此之外,GCC对剩下的寄存器中哪些会被当前的内嵌汇编修改一无所知。所以如果你真的在当前内嵌汇编语句中修改了它们,那么就最好“破坏描述部分”中声明它们,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。

在“破坏描述部分”中指定这些寄存器的方法很简单,你只需要将寄存器的名字使用双引号引起来。如果有多个寄存器需要声明,你需要在任意两个声明之间用逗号隔开。比如:
__asm__("movl %0, %%ebx; popl %%ecx" : : "a"(foo) : "%ebx", "%ecx" );
注意准备在“破坏描述部分”声明的寄存器必须使用完整的寄存器名称,在寄存器名称前面使用的“%”是可选的。
另外需要注意的是,如果你在“破坏描述部分”声明了一个寄存器,那么这个寄存器将不能再被用做当前内嵌汇编语句的输入或输出操作表达式的寄存器约束,如果输入或输出操作表达式的寄存器约束被指定为"r"或"g",GCC也不会选择已经被声明在“破坏描述部分”中的寄存器。比如:
__asm__("movl %0, %%ebx" : : "a"(foo) : "%eax", "%ebx");
此例中,由于输出操作表达式"a"(foo)的寄存器约束已经指定了%eax寄存器,那么再在“破坏描述部分”中指定"%eax"就是非法的。编译时,GCC会给出编译错误。

除了寄存器的内容会被改变,内存的内容也可以被修改。如果一个“汇编语句模板”中的指令对内存进行了修改,或者在此内嵌汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其输出操作表达式使用"m"约束,这种情况下你需要在“破坏描述部分”使用字符串"memory"向GCC声明:“在这里,内存发生了或可能发生了改变”。例如:

1
2
3
4
5
6
7
8
9
10
11
void * memset(void * s, char c, size_t count)
{
	__asm__("cld\n\t"
	"rep\n\t"
	"stosb"
	: /* no output */
	: "a"(c), "D"(s), "c"(count)
	: "%ecx", "%edi", "memory");

	return s;
}

此例实现了标准函数库memset,其内嵌汇编中的stosb对内存进行了改动,而其被修改的内存地址s被指定装入%edi,没有任何输出操作表达式使用了"m"约束,以指定内存地址s处的内容发生了改变。所以在其“破坏描述部分”使用"memory"向GCC声明:内存内容发生了变动。

如果一个内嵌汇编语句的“破坏描述部分”存在"memory",那么GCC会保证在此内嵌汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内嵌汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个时候寄存器中的拷贝已经很可能和内存处的内容不一致了。

当一个“汇编语句模板”中包含影响eflags寄存器中的条件标志,那么需要在“破坏描述部分”中使用"cc"来声明这一点。这些指令包括adc,div,popfl,btr,bts等等,另外,当包含call指令时,由于你不知道你所call的函数是否会修改条件标志,为了稳妥起见,最好也使用"cc"。

Netfilter HOOK

下图是Netfilter的IPV4下的结构

可以看到这是基于早期版本内核的,如今内核挂载点的宏定义发生了变化,从NF_IP_XXX => NF_INET_XXX

每个注册的钩子函数经过处理后都将返回下列值之一,告知Netfilter核心代码处理结果,以便对报文采取相应的动作:
NF_ACCEPT:继续正常的报文处理;
NF_DROP:将报文丢弃;
NF_STOLEN:由钩子函数处理了该报文,不要再继续传送;
NF_QUEUE:将报文入队,通常交由用户程序处理;
NF_REPEAT:再次调用该钩子函数。

// code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/net.h>
#include <net/tcp.h>
#include <linux/skbuff.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <net/ip_vs.h>
#include <net/sock.h>
#include <linux/gfp.h>
#include <linux/kallsyms.h>
#include <linux/version.h>

static unsigned int test_runit(unsigned int hooknum,
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 32)
		truct sk_buff **skb,
#else
		struct sk_buff *skb,
#endif
		const struct net_device *in,
		const struct net_device *out,
		int (*okfn)(struct sk_buff *))
{
	...
	return NF_ACCEPT;
}

static struct nf_hook_ops hook_test = {
	.hook    = test_runit,
	.owner    = THIS_MODULE,
	.pf    = PF_INET,
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 32)
	.hooknum        = NF_IP_LOCAL_OUT,
#else
	.hooknum        = NF_INET_LOCAL_OUT,
#endif
	.priority       = 100,
};

static int  __init test_start_init(void)
{
	printk("Hi test pre\n");
	nf_register_hook(&hook_test);
	return 0;
}

static void __exit test_start_exit(void)
{
	nf_unregister_hook(&hook_test);
	printk("Bye test pre\n");
}
module_init(test_start_init);
module_exit(test_start_exit);
MODULE_LICENSE("GPL");

数据交换sysctl + 定时器(code)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>

#include <linux/sysctl.h>
#include <linux/timer.h>

int value;

static struct ctl_table debug_table[] = {
	{
		.ctl_name       = CTL_UNNUMBERED,
		.procname       = "value",
		.data           = &value,
		.maxlen         = sizeof(value),
		.mode           = 0644,
		.proc_handler   = &proc_dointvec, },
	{ },
};

static struct ctl_table ws_dir_table[] = {
	{
		.ctl_name       = CTL_UNNUMBERED,
		.procname        = "debug",
		.mode            = 0555,
		.child           = debug_table, },
	{ },
};

static struct ctl_table ipv4_dir_table[] = {
	{
		.ctl_name       = NET_IPV4,
		.procname    = "ipv4",
		.mode        = 0555,
		.child       = ws_dir_table, },
	{ },
};

static ctl_table net_dir_table[] = {
	{
		.ctl_name       = CTL_NET,
		.procname    = "net",
		.mode        = 0555,
		.child           = ipv4_dir_table, },
	{ },
};

struct timer_list timer_last_stat;
static void output_value(unsigned long data)
{
	printk("value = %d\n", value);
	mod_timer(&timer_last_stat, jiffies+HZ*5);
}

struct ctl_table_header *ctl_header = NULL;
static int __init file_test_init(void)
{
	printk("sysctl test init\n");
	value = 111;
	ctl_header= register_sysctl_table (net_dir_table, 0);
	if(!ctl_header){
		printk(KERN_ERR"SYNPROXY: sp_sysctl_init() calls failed.");
		return -1;
	}
	setup_timer(&timer_last_stat, output_value, 0);
	mod_timer(&timer_last_stat, jiffies+HZ*5);
	return 0;
}

static void __exit file_test_exit(void)
{
	if (ctl_header)
		unregister_sysctl_table(ctl_header);
	del_timer(&timer_last_stat);
	printk("sysctl test exit\n");
}

module_init(file_test_init);
module_exit(file_test_exit);

MODULE_LICENSE("GPL");

1
2
3
4
5
6
7
8
$ dmesg
...
value = 111
$ echo 123 > /proc/sys/net/ipv4/debug/value
$ dmesg
...
value = 111
value = 123

内核定时器的使用

LINUX内核定时器是内核用来控制在未来某个时间点(基于jiffies)调度执行某个函数的一种机制,其实现位于 <linux/timer.h> 和 kernel/timer.c 文件中。

被调度的函数肯定是异步执行的,它类似于一种“软件中断”,而且是处于非进程的上下文中,所以调度函数必须遵守以下规则:
1. 没有 current 指针、不允许访问用户空间。因为没有进程上下文,相关代码和被中断的进程没有任何联系。
2. 不能执行休眠(或可能引起休眠的函数)和调度。
3. 任何被访问的数据结构都应该针对并发访问进行保护,以防止竞争条件。

内核定时器的调度函数运行过一次后就不会再被运行了(相当于自动注销),但可以通过在被调度的函数中重新调度自己来周期运行。

在SMP系统中,调度函数总是在注册它的同一CPU上运行,以尽可能获得缓存的局域性。

定时器API

内核定时器的数据结构

1
2
3
4
5
6
7
8
9
10
struct timer_list {
  struct list_head entry;
 
  unsigned long expires;
  void (*function)(unsigned long);
  unsigned long data;
 
  struct tvec_base *base;
  /* ... */
};

其中 expires 字段表示期望定时器执行的 jiffies 值,到达该 jiffies 值时,将调用 function 函数,并传递 data 作为参数。当一个定时器被注册到内核之后,entry 字段用来连接该定时器到一个内核链表中。base 字段是内核内部实现所用的。 需要注意的是 expires 的值是32位的,因为内核定时器并不适用于长的未来时间点。

初始化

在使用 struct timer_list 之前,需要初始化该数据结构,确保所有的字段都被正确地设置。初始化有两种方法。

方法一:
1
DEFINE_TIMER(timer_name, function_name, expires_value, data);

该宏会静态创建一个名叫 timer_name 内核定时器,并初始化其 function, expires, name 和 base 字段。

方法二:
1
2
3
struct timer_list mytimer;
setup_timer(&mytimer, (*function)(unsigned long), unsigned long data);
mytimer.expires = jiffies + 5*HZ;
方法三:
1
2
3
4
5
struct timer_list mytimer;
init_timer(&mytimer);
  mytimer ->timer.expires = jiffies + 5*HZ;
  mytimer ->timer.data = (unsigned long) dev;
  mytimer ->timer.function = &corkscrew_timer; /* timer handler */

通过init_timer()动态地定义一个定时器,此后,将处理函数的地址和参数绑定给一个timer_list,
注意,无论用哪种方法初始化,其本质都只是给字段赋值,所以只要在运行 add_timer() 之前,expires, function 和 data 字段都可以直接再修改。
关于上面这些宏和函数的定义,参见 include/linux/timer.h。

注册

定时器要生效,还必须被连接到内核专门的链表中,这可以通过 add_timer(struct timer_list *timer) 来实现。

重新注册

要修改一个定时器的调度时间,可以通过调用 mod_timer(struct timer_list *timer, unsigned long expires)。mod_timer() 会重新注册定时器到内核,而不管定时器函数是否被运行过。

注销

注销一个定时器,可以通过 del_timer(struct timer_list timer) 或 del_timer_sync(struct timer_list timer)。其中 del_timer_sync 是用在 SMP 系统上的(在非SMP系统上,它等于del_timer),当要被注销的定时器函数正在另一个 cpu 上运行时,del_timer_sync() 会等待其运行完,所以这个函数会休眠。另外还应避免它和被调度的函数争用同一个锁。对于一个已经被运行过且没有重新注册自己的定时器而言,注销函数其实也 没什么事可做。

1
int timer_pending(const struct timer_list *timer)

这个函数用来判断一个定时器是否被添加到了内核链表中以等待被调度运行。注意,当一个定时器函数即将要被运行前,内核会把相应的定时器从内核链表中删除(相当于注销)

例子1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
 
struct timer_list mytimer;
static void myfunc(unsigned long data)
{
	printk("%s/n", (char *)data);
	mod_timer(&mytimer, jiffies + 2*HZ);
}
 
static int __init mytimer_init(void)
{
	setup_timer(&mytimer, myfunc, (unsigned long)"Hello, world!");
	mytimer.expires = jiffies + HZ;
	add_timer(&mytimer);
	return 0;
}
 
static void __exit mytimer_exit(void)
{
	del_timer(&mytimer);
}
module_init(mytimer_init);
module_exit(mytimer_exit);
例子2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static struct timer_list power_button_poll_timer;
static void power_button_poll(unsigned long dummy)
{
	if (gpio_line_get(N2100_POWER_BUTTON) == 0) {
		ctrl_alt_del();
		return;
	}
	power_button_poll_timer.expires = jiffies + (HZ / 10);
	add_timer(&power_button_poll_timer);
}
static void __init n2100_init_machine(void)
{
...
	init_timer(&power_button_poll_timer);
	power_button_poll_timer.function = power_button_poll;
	power_button_poll_timer.expires = jiffies + (HZ / 10);
	add_timer(&power_button_poll_timer);
}
例子3:

设备open时初始化和注册定时器

1
2
3
4
5
6
7
8
9
10
static int corkscrew_open(struct net_device *dev)
{
...
	  init_timer(&vp->timer);    
	  vp->timer.expires = jiffies + media_tbl[dev->if_port].wait;
	  vp->timer.data = (unsigned long) dev;
	  vp->timer.function = &corkscrew_timer; /* timer handler */
	  add_timer(&vp->timer);
...
}

定时器超时处理函数,对定时器的超时时间重新赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void corkscrew_timer(unsigned long data)
{
...
	vp->timer.expires = jiffies + media_tbl[dev->if_port].wait;
	add_timer(&vp->timer);
...
}
 
设备close时删除定时器
static int corkscrew_close(struct net_device *dev)
{
...
	del_timer(&vp->timer);
...
}
例子4:

本例子用DEFINE_TIMER静态创建定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <linux/module.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/timer.h>
#include <linux/leds.h>
static void ledtrig_ide_timerfunc(unsigned long data);
DEFINE_LED_TRIGGER(ledtrig_ide);
static DEFINE_TIMER(ledtrig_ide_timer, ledtrig_ide_timerfunc, 0, 0);
static int ide_activity;
static int ide_lastactivity;
void ledtrig_ide_activity(void)
{
	ide_activity++;
	if (!timer_pending(&ledtrig_ide_timer))
		mod_timer(&ledtrig_ide_timer, jiffies + msecs_to_jiffies(10));
}
EXPORT_SYMBOL(ledtrig_ide_activity);
static void ledtrig_ide_timerfunc(unsigned long data)
{
	if (ide_lastactivity != ide_activity) {
		ide_lastactivity = ide_activity;
		led_trigger_event(ledtrig_ide, LED_FULL);
		mod_timer(&ledtrig_ide_timer, jiffies + msecs_to_jiffies(10));
	} else {
		led_trigger_event(ledtrig_ide, LED_OFF);
	}
}
static int __init ledtrig_ide_init(void)
{
	led_trigger_register_simple("ide-disk", &ledtrig_ide);
	return 0;
}
static void __exit ledtrig_ide_exit(void)
{
	led_trigger_unregister_simple(ledtrig_ide);
}
module_init(ledtrig_ide_init);
module_exit(ledtrig_ide_exit);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
add_timer() -- 将定时器添加到定时器等待队列中
用add_timer()函数来看timer_base的作用
static inline void add_timer(struct timer_list *timer)
{
	BUG_ON(timer_pending(timer));
	__mod_timer(timer, timer->expires);
}

int __mod_timer(struct timer_list *timer, unsigned long expires)
{
	tvec_base_t *base, *new_base;
	unsigned long flags;
	int ret = 0;
	timer_stats_timer_set_start_info(timer);
	BUG_ON(!timer->function);
	base = lock_timer_base(timer, &flags);
如果timer已经放到定时链表中,则释放开
|--------------------------------|
|   if (timer_pending(timer)) { -|
|       detach_timer(timer, 0); -|
|       ret = 1;                 |
|   }                            |
|--------------------------------|
获取当前CPU的timer base
|-----------------------------------------|
|   new_base = __get_cpu_var(tvec_bases); |
|-----------------------------------------|
如果当前CPU的timer base不是当前timer中的base, 更新timer的base
|----------------------------------------------------|
|   if (base != new_base) {                          |
|       if (likely(base->running_timer != timer)) { -|
|           timer->base = NULL;                      |
|           spin_unlock(&base->lock);                |
|           base = new_base;                         |
|           spin_lock(&base->lock);                  |
|           timer->base = base;                      |
|       }                                            |
|   }                                                |
|----------------------------------------------------|
给定时器timer设置超时时间;并添加该时钟
|-------------------------------------|
|   timer->expires = expires;         |
|   internal_add_timer(base, timer); -|
|-------------------------------------|
	spin_unlock_irqrestore(&base->lock, flags);
	return ret;
}
MODULE_LICENSE("GPL");