kk Blog —— 通用基础

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

内核定时器的使用

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");

用户空间和内核空间数据交换方式-sysctl

sysctl是一种用户应用来设置和获得运行时内核的配置参数的一种有效方式,通过这种 方式,用户应用可以在内核运行的任何时刻来改变内核的配置参数,也可以在任何时候获得内核的配置参数,通常,内核的这些配置参数也出现在proc文件系统 的/proc/sys目录下,用户应用可以直接通过这个目录下的文件来实现内核配置的读写操作,例如,用户可以通过

1
cat /proc/sys/net/ipv4/ip_forward 

来得知内核IP层是否允许转发IP包,用户可以通过

1
echo 1 > /proc/sys/net/ipv4/ip_forward 

把内核 IP 层设置为允许转发 IP 包,即把该机器配置成一个路由器或网关。 一般地,所有的 Linux 发布也提供了一个系统工具 sysctl,它可以设置和读取内核的配置参数,但是该工具依赖于 proc 文件系统,为了使用该工具,内核必须支持 proc 文件系统。下面是使用 sysctl 工具来获取和设置内核配置参数的例子:

1
2
3
4
5
6
# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
# sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1 

注意,参数 net.ipv4.ip_forward 实际被转换到对应的 proc 文件/proc/sys/net/ipv4/ip_forward,选项 -w 表示设置该内核配置参数,没有选项表示读内核配置参数,用户可以使用 sysctl -a 来读取所有的内核配置参数,对应更多的 sysctl 工具的信息,请参考手册页 sysctl(8)。

但是 proc 文件系统对 sysctl 不是必须的,在没有 proc 文件系统的情况下,仍然可以,这时需要使用内核提供的系统调用 sysctl 来实现对内核配置参数的设置和读取。

在源代码中给出了一个实际例子程序,它说明了如何在内核和用户态使用sysctl。头文件 sysctl-exam.h 定义了 sysctl 条目 ID,用户态应用和内核模块需要这些 ID 来操作和注册 sysctl 条目。内核模块在文件 sysctl-exam-kern.c 中实现,在该内核模块中,每一个 sysctl 条目对应一个 struct ctl_table 结构,该结构定义了要注册的 sysctl 条目的 ID(字段 ctl_name),在 proc 下的名称(字段procname),对应的内核变量(字段data,注意该该字段的赋值必须是指针),条目允许的最大长度(字段maxlen,它主要用于 字符串内核变量,以便在对该条目设置时,对超过该最大长度的字符串截掉后面超长的部分),条目在proc文件系统下的访问权限(字段mode),在通过 proc设置时的处理函数(字段proc_handler,对于整型内核变量,应当设置为&proc_dointvec,而对于字符串内核变量, 则设置为 &proc_dostring),字符串处理策略(字段strategy,一般这是为&sysctl_string)。

sysctl 条目可以是目录,此时 mode 字段应当设置为 0555,否则通过 sysctl 系统调用将无法访问它下面的 sysctl 条目,child 则指向该目录条目下面的所有条目,对于在同一目录下的多个条目,不必一一注册,用户可以把它们组织成一个 struct ctl_table 类型的数组,然后一次注册就可以,但此时必须把数组的最后一个结构设置为NULL,即

1
2
3
{
	.ctl_name = 0
}

注册sysctl条目使用函数register_sysctl_table(struct ctl_table *, int),第一个参数为定义的struct ctl_table结构的sysctl条目或条目数组指针,第二个参数为插入到sysctl条目表中的位置,如果插入到末尾,应当为0,如果插入到开头, 则为非0。内核把所有的sysctl条目都组织成sysctl表。

当模块卸载时,需要使用函数unregister_sysctl_table(struct ctl_table_header *)解注册通过函数register_sysctl_table注册的sysctl条目,函数register_sysctl_table在调用成功时返 回结构struct ctl_table_header,它就是sysctl表的表头,解注册函数使用它来卸载相应的sysctl条目。 用户态应用sysctl-exam-user.c通过sysctl系统调用来查看和设置前面内核模块注册的sysctl条目(当然如果用户的系统内核已经 支持proc文件系统,可以直接使用文件操作应用如cat, echo等直接查看和设置这些sysctl条目)。

下面是运行该模块与应用的输出结果示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# insmod ./sysctl-exam-kern.ko
# cat /proc/sys/mysysctl/myint
0
# cat /proc/sys/mysysctl/mystring
# ./sysctl-exam-user
mysysctl.myint =0
mysysctl.mystring =""
# ./sysctl-exam-user 100"Hello, World"
old value: mysysctl.myint =0
newvalue: mysysctl.myint =100
old vale: mysysctl.mystring =""
newvalue: mysysctl.mystring ="Hello, World"
# cat /proc/sys/mysysctl/myint
100
# cat /proc/sys/mysysctl/mystring
Hello, World
#

struct ctl_table是相关的主要的数据结构。定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ctl_table
{
	int ctl_name;            /* Binary ID */
	const char *procname;        /* Text ID for /proc/sys, or zero */
	void *data;
	int maxlen;
	mode_t mode;
	struct ctl_table *child;
	struct ctl_table *parent;    /* Automatically set */
	proc_handler *proc_handler;    /* Callback for text formatting */
	ctl_handler *strategy;        /* Callback function for all r/w */
	void *extra1;
	void *extra2;
};

每个这样的数据结构,都对应了/proc/sys目录下的一项内核参数。

其中各字段的意义如下:
ctl_name: 唯一标识此表项的一个整数。
proc_name: 相对于/proc/sys目录下对应的参数名称。
data: 一个void *指针,指向与这个表项相关联的数据的指针。
maxlen: 可以读取或者写入data的最大字节数。eg:data指向的是一个int型参数,则maxlen一般为sizeof(int)。
mode: 文件的权限。
child: 如果此数据结构对应的表项为一目录,则chind是指向其子表项的指针。
parent: 同child,指向其所在目录对应的ctl_table。
proc_handler: 如果是通过/proc/sys文件读写内核运行时的参数,则执行此操作。
strategy: 如果是通过系统调用sysctl对内核参数进行读写,则调用此函数。
extra1和extra2可以指向处理这个表项时的任何补充数据。

下面分别简单说下这两个函数大概的执行流程:
proc_handler:
一般情况下,proc_handler指向proc_dostring(操作数为string)或者proc_dointvec(操作数为int)。

strategy:
sysctl ==> sys_sysctl ==> do_sysctl ==> parse_table ==> do_sysctl_strategy ==> ctl_table中strategy对应的操作。


示例:

头文件:sysctl-exam.h:

1
2
3
4
5
6
7
8
9
10
11
//header: sysctl-exam.h
#ifndef _SYSCTL_EXAM_H
#define _SYSCTL_EXAM_H
#include <linux/sysctl.h>
#define MY_ROOT (CTL_CPU + 10)
#define MY_MAX_SIZE 256
enum {
	MY_INT_EXAM = 1,
	MY_STRING_EXAM = 2,
};
#endif

内核模块代码 sysctl-exam-kern.c:

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
//kernel module: sysctl-exam-kern.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sysctl.h>
#include "sysctl-exam.h"

static char mystring[256];
static int myint;

static struct ctl_table my_sysctl_exam[] = {
	{
		.ctl_name       = MY_INT_EXAM,
		.procname       = "myint",
		.data           = &myint,
		.maxlen         = sizeof(int),
		.mode           = 0666,
		.proc_handler   = &proc_dointvec,
	},
	{
		.ctl_name       = MY_STRING_EXAM,
		.procname       = "mystring",
		.data           = mystring,
		.maxlen         = MY_MAX_SIZE,
		.mode           = 0666,
		.proc_handler   = &proc_dostring,
		.strategy       = &sysctl_string,
	},
	{
		.ctl_name = 0
	}
};

static struct ctl_table my_root = {
	.ctl_name       = MY_ROOT,
	.procname       = "mysysctl",
	.mode           = 0555,
	.child          = my_sysctl_exam,
};

static struct ctl_table_header * my_ctl_header;

static int __init sysctl_exam_init(void)
{
	my_ctl_header = register_sysctl_table(&my_root, 0);

	return 0;
}

static void __exit sysctl_exam_exit(void)
{
	unregister_sysctl_table(my_ctl_header);
}

module_init(sysctl_exam_init);
module_exit(sysctl_exam_exit);
MODULE_LICENSE("GPL");

用户程序 sysctl-exam-user.c:

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
83
84
85
86
87
88
89
90
91
92
93
94
//application: sysctl-exam-user.c
#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/sysctl.h>
#include "sysctl-exam.h"
#include <stdio.h>
#include <errno.h>

_syscall1(int, _sysctl, struct __sysctl_args *, args);

int sysctl(int *name, int nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen)
{
	struct __sysctl_args args={name,nlen,oldval,oldlenp,newval,newlen};
	return _sysctl(&args);
}

#define SIZE(x) sizeof(x)/sizeof(x[0])
#define OSNAMESZ 100

int oldmyint;
int oldmyintlen;

int newmyint;
int newmyintlen;

char oldmystring[MY_MAX_SIZE];
int oldmystringlen;

char newmystring[MY_MAX_SIZE];
int newmystringlen;

int myintctl[] = {MY_ROOT, MY_INT_EXAM};
int mystringctl[] = {MY_ROOT, MY_STRING_EXAM};

int main(int argc, char ** argv)
{
	if (argc < 2) 
	{
		oldmyintlen = sizeof(int);
		if (sysctl(myintctl, SIZE(myintctl), &oldmyint, &oldmyintlen, 0, 0)) {
			perror("sysctl");
			exit(-1);
		}
		else {
			printf("mysysctl.myint = %d\n", oldmyint);
		}

		oldmystringlen = MY_MAX_SIZE;
		if (sysctl(mystringctl, SIZE(mystringctl), oldmystring, &oldmystringlen, 0, 0)) {
			perror("sysctl");
			exit(-1);
		}
		else {
			printf("mysysctl.mystring = \"%s\"\n", oldmystring);
		}
	}
	else if (argc != 3) 
	{
		printf("Usage:\n");
		printf("\tsysctl-exam-user\n");
		printf("Or\n");
		printf("\tsysctl-exam-user aint astring\n");
	}
	else 
	{
		newmyint = atoi(argv[1]);
		newmyintlen = sizeof(int);
		oldmyintlen = sizeof(int);

		strcpy(newmystring, argv[2]);
		newmystringlen = strlen(newmystring);
		oldmystringlen = MY_MAX_SIZE;

		if (sysctl(myintctl, SIZE(myintctl), &oldmyint, &oldmyintlen, &newmyint, newmyintlen)) {
			perror("sysctl");
			exit(-1);
		}
		else {
			printf("old value: mysysctl.myint = %d\n", oldmyint);
			printf("new value: mysysctl.myint = %d\n", newmyint);
		}


		if (sysctl(mystringctl, SIZE(mystringctl), oldmystring, &oldmystringlen, newmystring, newmystringlen)) {
			perror("sysctl");
			exit(-1);
		}
		else {
			printf("old vale: mysysctl.mystring = \"%s\"\n", oldmystring);
			printf("new value: mysysctl.mystring = \"%s\"\n", newmystring);
		}
	}
	exit(0);
}

Makefile与Shell的问题

大概只要知 道Makefile的人,都知道Makefile可以调用Shell脚本。但是在实际使用时,并不那么简单,一些模棱两可的地方可能会让你抓狂。你若不信,可以先看几个例子,想象一下这些这些例子会打印什么内容,记下你想象的结果,然后在计算机上运行这些例子,对照看一下。

示例一:

1
2
3
4
5
6
7
if [ "$(BUILD)" = "debug" ]; then
	echo "build debug"; 
else
	echo "build release";
fi
all:
	echo "done"

示例二:

1
2
3
all:
	@CC=arm-linux-gcc
	@echo $(CC)

示例三:

1
2
3
CC=arm-linux-gcc
all:
	@echo $(CC)

示例四:

1
2
3
4
5
6
SUBDIR=src example
all:
	@for subdir in $(SUBDIR);
	do
		echo "building " $(subdir);
	done

说明:

1.Shell脚本在target里才有效,其它地方都被忽略掉了。所以示例一中,”build debug”之类的字符串根本打印不出来。示例一的正确写法是: 示例一:

1
2
3
4
5
6
7
all:
	if [ "$(BUILD)" = "debug" ]; then
		echo "build debug";
	else
		echo "build release";
	fi
	echo "done"

2.make把每一行Shell脚本当作一个独立的单元,它们在单独的进程中运行。示例二中,两行Shell脚本在两个莫不相干的进程里运行,第一个进程把 CC设置为arm-linux-gcc,第二个进程是不知道的,所以打印的结果自然不是arm-linux-gcc了。示例二的正确写法是:
示例二:

1
2
3
4
5
6
all:
	@CC=arm-linux-gcc; echo $(CC)
或者:
all:
	@CC=arm-linux-gcc;
	echo $(CC)

3.make在调用Shell之前先进行预处理,即展开所有Makefile的变量和函数。这些变量和函数都以$开头。示例三中,Shell拿的脚本实际上是echo arm-linux-gcc,所以打印结果正确。

4.make预处理时,所有以$开头的,它都不会放过。要想引用Shell自己的变量,应该以$$开头。另外要注意,Shell自己的变量是不需要括号的。示例四的正确写法是:
示例四:

1
2
3
4
5
6
SUBDIR=src example
all:
	@for subdir in $(SUBDIR);
	do
		echo "building " $$subdir;
	done

字节序和比特序

字节序和比特序,因为比特序对所有代码(包括汇编)是透明的,所以对于小端系统,有说是用大端比特序,也有说是用小端比特序。
下面是copy一部分觉得靠谱的内容:
大小端 我们对"endianness"这个名词估计都很熟悉了。它首先被Danny Cohen于1980引入,用来表述计算机系统表示多字节整数的方式。 endianness分为两种:大端和小端。(从字节序的角度来看)大端方式是将整数中最高位byte存放在最低地址中。而小端方式则相反,将整数中的最高位byte存放在最高地址中。 对于某个确定的计算机系统,比特序通常与字节序保持一致。换言之,在大端系统中,每个byte中最高位bit存放在内存最低位;在小端系统中,最低位bit存放在内存最低位。 正如大部分人是按照从左至右的顺序书写数字,一个多字节整数的内存布局也应该遵循同样的方式,即从左至右为数值的最高位至最低位。正如我们在下面的例子中所看到的,这是书写整数最清晰的方式。

根据上述规则,我们按以下方式分别在大端和小端系统中值为0x0a0b0c0d的整数。 在大端系统中书写整数:

1
2
3
4
5
byte  addr   0   1   2   3
bit offset  01234567 01234567 01234567 01234567

    binary  00001010 00001011 00001100 00001101
      hex      0a       0b       0c       0d

在小端系统中书写整数(认真看)

1
2
3
4
5
byte  addr   0   1   2   3
bit offset  01234567 01234567 01234567 01234567

    binary  10110000 00110000 11010000 01010000
      hex      d0       c0       b0       a0

说明字节序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>  
int main (void)  
{  
	union b  
	{  
		short k;  //测试环境short占2字节  
		char i[2];  //测试环境char占1字节  
	}*s,a;  
	s=&a;  
	s->i[0]=0x41;  
	s->i[1]=0x52;  
	printf("%x\n",s->k);  
	return 0;  
}

输出:5241


self 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
#include <stdio.h>
union W
{
	struct Y
	{
		unsigned int s1:4;
		unsigned int s2:8;
		unsigned int s3:20;
	} y;
	unsigned int c;
} w;

union V 
{
	struct X
	{
		unsigned char s1:3;
		unsigned char s2:3;
		unsigned char s3:2;
	} x;
	unsigned char c;
} v;

int main()
{
	w.c = 0x12345678;
	printf("%x %x %x %x\n", w.c, w.y.s1, w.y.s2, w.y.s3); 

	v.c = 100;
	printf("%d %x %x %x\n", v.c, v.x.s1, v.x.s2, v.x.s3); 
	return 0;
}

输出:
12345678 8 67 12345
100 4 4 1

100 = (01100100)2
因为字节序是小端的所以第一行输出说明:位域变量从左到右分配位,所以第二行的输出的位域变量也应该从左到右分配位。所以
100 = 001 001 10 (小端比特序二进制)
对应: s1 s2 s3 (位域变量从左到右分配位)

符合。

jmp指令对应的机器码

短跳转和近跳转指令中包含的操作数都是相对于(E)IP的偏移,而远跳转指令中包含的是目标的绝对地址,所以短/近跳转会出现跳至同一目标的指令机器码不同,不仅会不同,而且应该不同。而远跳转中包含的是绝对地址,因此转移到同一地址的指令机器码相同

绝对跳转/调用指令中的内存操作数必须以’*’为前缀,否则gas总是认为是相对跳转/调用指令,而且gas汇编程序自动对跳转指令进行优化,总是使用尽可能小的跳转偏移量。如果8比特的偏移量无法满足要求的话,as会使用一个32位的偏移量,as汇编程序暂时还不支持16位的跳转偏移量,所以对跳转指令使用’addr16’前缀是无效的。还有一些跳转指令只支持8位的跳转偏移量,这些指令是:’jcxz’,’jecxz’,’loop’,’loopz’,’loope’,’loopnz’’loopne’如果你在汇编中使用了这些指令,用gas的汇编可能会出错,因为gcc在编译过程中不产生这些指令,所以在c语言中不必担心这些问题。

1
2
ffffffff88873036      e8 ff ff 5f c6   =>  call XX        // e8 = call
ffffffff8887303a      ......

相当于:目标地址 - ffffffff8887303a(RIP, 指向下一条指令) = ffffffffffff5fc6 (这个是负数,以补码形式展示)
所以: 目标地址 = ffffffff88869000

  • 即: ffffffff88869000 - ffffffff8887303a + ffffffffffffffff + 1 = ffffffffffff5fc6
  • 可以用 unsigned long 类型来计算,让它自然溢出就好了,(unsigned long)func1 - ((unsigned long)func2 + 0x偏移)

先计算好偏移,再替换call地址,就偷偷的改了调用。

附:若为e8 00 00 00 00 则可以同过模块读取 00 00 00 00 的实际值