存档在 2011年5月

内核模块的实现

2011年5月29日

内核模块再学习之模块的实现

如果你对内核模块编程已经有了简单的认识,那么可以更进一步学习模块在内核中的实现。对于每个内核模块来说,系统都为其分配一块内存区,这块内存区包括:一个module结构、唯一表示模块名称的字符串和实现模块功能的代码。

module结构中包含众多字段,从多个方面对内核模块进行描述,这一点如同task_struct结构对一个进程的全面描述一样。我们接下来只关注几个与我们平时简单的内核模块编程相关的字段。

enum module_state state:显示内核模块的状态;该枚举类型包含三种模块状态:MODULE_STATE_COMING、MODULE_STATE_LIVE和MODULE_STATE_GOING。分别表示正在加载模块、模块已经加载并且可用、正在卸载模块。
struct list_head list:内核中的所有模块结构通过双链表组织在一起,该字段表示当前模块在双链表中的节点。
char name[MODULE_NAME_LEN]:模块的名称,每个模块的名称都是唯一的。
int (*init)(void):init是一个函数指针,它指向模块的加载函数。
void (*exit)(void):exit是一个函数指针,它指向模块的卸载函数。
unsigned int core_size:用于模块核心函数与数据结构的动态内存区大小。

通过上述的字段分析,我们知道内核中的模块是通过双链表进行组织的。因此,我们就可以通过双链表的操作函数来打印内核中每个模块的相关信息。不过,模块链表的头指针modules并未被导出,所以我们通过在符号表中查看modules在内存中的地址来对其进行操作(关于通过符号表得到内核中某个未导出符号的地址可查看这里的文章)。整个程序的关键代码参考如下。

#define modules 0xc0771264

static int __init print_module_init(void)
{
	struct module *p;
	struct list_head *module_head;
	struct list_head *pos;
	int i = 0;

	printk("print_module module is starting..\n");

	module_head = (struct list_head *)modules;
	list_for_each(pos, module_head) {
		p = list_entry(pos, struct module, list);
		printk("%3d %20s %10d\n", ++i, p->name, p->core_size);
	}

	return 0;
}

遍历的方法很简单,先得到模块链表的头指针module_head,再通过list_for_each函数依次遍历每个内核模块。我们在此打印的模块信息是模块的名称和模块的大小,其输出结果和lsmod命令的输出结果相同。如下所示。

[ 7405.518605] print_module module is starting..
[ 7405.518610]   1         print_module        632
[ 7405.518613]   2                hello        830
[ 7405.518616]   3          binfmt_misc       6587
[ 7405.518618]   4           vboxnetadp       6808
[ 7405.518621]   5           vboxnetflt      18753
[ 7405.518624]   6                  via      37920
[ 7405.518626]   7                  drm     162345
[ 7405.518629]   8              vboxdrv     229952
…………

完整代码可以在这里下载。

使用list_head建立双联表

2011年5月24日

内核中与链表有关的操作都集中在list.h文件中,该文件不仅定义了链表数据结构,而且包含大量对链表操作的函数。

下面通过一个内核模块简单说明内核双链表的用法。该内核模块的加载函数首先建立了一个具有N个节点的双链表,然后再遍历该链表;在该内核模块卸载函数中,依次将每个节点从双链表中删除,并释放每个节点所占用的内核空间的内存。

在具体说明该程序之前,有必要搞清楚两个概念:链表中的数据节点和链表节点。内核在list.h中定义的链表节点list_head称之为链表节点。该结构除了指向前后节点的指针外,并不包含任何数据变量。因此,在实际使用的过程中,我们必须将所需数据和链表节点封装在一个数据结构中,这个新的数据结构就是数据节点。比如,在示例程序中,我们的数据节点除了包含链表节点外,还包含一个数据成员num。

#define N 10
//链表节点
struct numlist {
	int num;//数据
	struct list_head list;//指向双联表前后节点的指针
};
struct numlist numhead;//头节点

在使用双联表的操作函数时也应该注意数据节点和链表节点之间的差异。所有与双联表有关的操作函数都是对链表节点的操作,也就是说我们应该对这些函数传入链表节点(或指针)而不是数据节点。

在加载函数中,我们通过INIT_LIST_HEAD宏初始化链表的头指针。通过查看该宏的定义可知,必须向其传入一个list_head结构的指针变量,因此我们将&numhead.list传入其中。

接着,通过循环分别创建N个数据节点,并在创建完每个节点后,将其加入整个双链表中。将节点加入双链表的过程是通过list_add_tail函数完成的,该函数将新节点加入到双链表的末尾。

static int __init doublelist_init(void)
{
	//初始化头节点
	struct numlist *listnode;//每次申请链表节点时所用的指针
	struct list_head *pos;
	struct numlist *p;
	int i;

	printk("doublelist is starting...\n");
	INIT_LIST_HEAD(&numhead.list);

	//建立N个节点,依次加入到链表当中
		for (i = 0; i < N; i++) {
		listnode = (struct numlist *)kmalloc(sizeof(struct numlist), GFP_KERNEL);
		listnode->num = i+1;
		list_add_tail(&listnode->list, &numhead.list);
		printk("Node %d has added to the doublelist...\n", i+1);
	}

加载函数创建完双链表后,紧接着又对该双链表进行了遍历操作。我们在上面说过双链表的操作函数是对链表节点list_head进行操作的,因此遍历宏list_for_each也是对链表节点的遍历。如果我们需要得到数据节点中每个成员的值,那么就必须获得当前数据节点的指针,也就是该节点的首地址。因此list_entry宏的作用就是通过当前正在遍历的链表节点的指针pos获得其所属数据节点的指针p。

这里特别说明一下list_entry宏的参数,pos所指向的list_head结构在numlist结构中的字段称为list。

	//遍历链表
	i = 1;
	list_for_each(pos, &numhead.list) {
		p = list_entry(pos, struct numlist, list);
		printk("Node %d's data:%d\n", i, p->num);
		i++;
	}

	return 0;
}

在卸载函数中将会依次删除链表中的节点。list_for_each_safe遍历宏是专门为删除链表结点而设计的,它在遍历当前节点对同时保存下一个节点的地址。因此在每次遍历的时候一方面使用list_del将当前链表节点从整个链表中删除,一方面使用kfree函数释放该数据节点所占的内存空间。

static void __exit doublelist_exit(void)
{
	struct list_head *pos, *n;
	struct numlist *p;
	int i;

	//依次删除N个节点
	i = 1;
	list_for_each_safe(pos, n, &numhead.list) {
		list_del(pos);//从双联表中删除当前节点
		p = list_entry(pos, struct numlist, list);//得到当前数据节点的首地址,即指针
		kfree(p);//释放该数据节点所占空间
		printk("Node %d has removed from the doublelist...\n", i++);
	}
	printk("doublelist is exiting..\n");
}

该实例程序的完整代码可在此下载。

对/proc文件系统进行读写操作

2011年5月19日

本博客之前的文章中多次涉及到/proc文件系统,下面的几条命令都在曾经的文章中出现过:

cat /proc/interrupts
cat /proc/devices
cat /proc/kallsyms | grep super_blocks

第一条命令用于查看系统内已注册的中断信息,包括中断号、已接受的手段请求和驱动器名称等;第二条命令用于查看系统内已注册的字符设备和块设备信息,包括设备号和设备名称;第三条命令用于在内核符号表中检索super_blocks符号的的地址,kallsyms文件包括内核中所有的标示符及其地址。

1.概述

proc即process的缩写,最初的proc文件系统只是存放进程的相关信息。但现在的/proc文件系统除此之外还包含系统的状态信息和配置信息。
通过ls命令就可以查看/proc文件系统所包含的内容。

edsionte@edsionte-desktop:/proc$ ls
1      1290   1469  1541  1627   19612  29    49    9          dri              mdstat          sys
10     13     1471  1544  1630   19613  3     5     908        driver           meminfo         sysrq-trigger
1013   1301   1474  1548  1632   19629  30    50    913        edsionte_procfs  misc            sysvipc
…………

其中以数字为名的目录即为系统中正在运行的进程信息,数字即为进程的pid。比如我们可以进入init进程的目录,查看它的地址空间:

edsionte@edsionte-desktop:/proc/1$ sudo cat maps
[sudo] password for edsionte:
00110000-00263000 r-xp 00000000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00263000-00264000 ---p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00264000-00266000 r--p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00266000-00267000 rw-p 00155000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00267000-0026a000 rw-p 00000000 00:00 0
0026a000-00272000 r-xp 00000000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00272000-00273000 r--p 00007000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00273000-00274000 rw-p 00008000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00471000-0048b000 r-xp 00000000 08:07 1048610    /sbin/init
…………

除了查看进程的相关信息,我们还可以通过打印相关文件来查看系统的当前运行状态。比如查看当前内存的使用情况:

edsionte@edsionte-desktop:/proc$ cat meminfo
MemTotal:         961368 kB
MemFree:          145264 kB
Buffers:           31648 kB
Cached:           297716 kB
SwapCached:        14436 kB
…………

总之,/proc文件系统相当于内核的一个快照,该目录下的所有信息都是动态的从正在运行的内核中读取。

基于这种原因,/proc文件系统就成为了用户和内核之间交互的接口。一方面,用户可以从/proc文件系统中读取很多内核释放出来的信息;另一方面,内核也可以在恰当的时候从用户那里得到输入信息,从而改变内核的相关状态和配置。

相比传统的文件系统,/proc是一种特殊的文件系统,即虚拟文件系统。这里的虚拟是强调/proc文件系统下的所有文件都存在于内存中而不是磁盘上。也就是说/proc文件系统只占用内存空间,而不占用系统的外存空间。

2.用户态和内核态之间的数据通信

既然内核的数据以/proc文件系统的形式呈现给用户,也就是说内核的信息以文件的形式存在于该文件系统中,那么/proc文件系统就应当提供一组接口对其内的文件进行读写操作。接下来我们以一个实际的内核模块程序easyProc.c为例,说明/proc文件系统的常用接口。该程序中依次创建了几个虚拟文件,然后在用户态对这些文件进行读写测试。

2.0数据结构

每个虚拟文件都对应一个proc_dir_entry类型的数据结构,该结构具体定义如下:

struct proc_dir_entry {
	const char *name;			// virtual file name
	mode_t mode;				// mode permissions
	uid_t uid;				// File's user id
	gid_t gid;				// File's group id
	struct inode_operations *proc_iops;	// Inode operations functions
	struct file_operations *proc_fops;	// File operations functions
	struct proc_dir_entry *parent;		// Parent directory
	...
	read_proc_t *read_proc;			// /proc read function
	write_proc_t *write_proc;		// /proc write function
	void *data;				// Pointer to private data
	atomic_t count;				// use count
	...
};

除了保存该虚拟文件的基本信息外,该结构中还有read_proc和write_proc两个字段,下文中将有详细说明。

2.1创建目录

/proc文件系统中创建一个目录对应的函数接口如下:
struct proc_dir_entry *proc_mkdir(const char *name,struct proc_dir_entry *parent);
其中name为要创建的目录名;parent为这个目录的父目录,当要创建的目录位于/proc下时此参数为空。比如我们使用该函数在/proc下创建一个目录edsionte_procfs。

#define MODULE_NAME "edsionte_procfs"
struct proc_dir_entry *example_dir;
	example_dir = proc_mkdir(MODULE_NAME, NULL);
	if (example_dir == NULL) {
		rv = -ENOMEM;
		goto out;
	}
2.2创建普通文件

在/proc文件系统中创建一个虚拟文件可以使用如下的函数:

 static inline struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent) ;

该函数中name为要创建的文件名;mode为创建文件的属性;parent指向该文件父目录的指针,如果创建的虚拟文件位于/proc下,则这个参数为NULL。

比如我们通过该函数在/proc/edsionte_procfs目录下创建一个虚拟文件foo,其权限为644。其中example_dir指向我们刚创建的目录文件edsionte_procfs。

struct proc_dir_entry  *foo_file;
	foo_file = create_proc_entry("foo", 0644, example_dir);
	if (foo_file == NULL) {
		rv = -ENOMEM;
		goto no_foo;
	}
2.3.创建符号链接文件

当我们需要在/proc文件系统下创建一个符号链接文件时,可使用如下接口:

struct proc_dir_entry *proc_symlink(const char *name, struct proc_dir_entry *parent, const char *dest);

name参数为要创建的符号链接文件名;parent为该符号链接文件的父目录;dest为符号链接所指向的目标文件。

下面的代码演示了如何通过该函数来对已存在的虚拟文件jiffies创建符号链接文件jiffies_too:

	symlink = proc_symlink("jiffies_too", example_dir, "jiffies");
	if (symlink == NULL) {
		rv = -ENOMEM;
		goto no_symlink;
	}

我们内核模块加载函数中完成上述几个虚拟文件的创建工作。

2.4.删除文件或目录

既然有创建虚拟文件的函数,必然也就有删除虚拟文件的函数接口:

void remove_proc_entry(const char *name, struct proc_dir_entry *parent);

该函数中的参数name和parent与上述函数的参数意义相同。
在示例程序中,我们在卸载函数中完成上述几个文件的删除工作:

	remove_proc_entry("jiffies_too", example_dir);
	remove_proc_entry("foo", example_dir);
	remove_proc_entry("MODULE_NAME", NULL);
2.5读写proc文件

如果只是创建了虚拟文件,那么它并不能被读写。为此,我们必须为每个虚拟文件挂接读写函数,如果该虚拟文件是只读的,那么只需挂载相应的读函数。

正如上面所述,每个虚拟文件对应的proc_dir_entry结构都有read_proc和write_proc两个字段,它们均为函数指针,其各自的类型定义如下:

 typedef int (read_proc_t)(char *page, char **start, off_t off, int count, int *eof, void *data);
  typedef int (write_proc_t)(struct file *file, const char __user *buffer, unsigned long count, void *data);

如果要实现对虚拟文件的读写,则需要实现上述两个函数接口。对于我们的示例程序,我们的实现方法如下:

static int proc_read_foobar(char *page, char **start, off_t off, int count, int *eof, void *data)
{
	int len;
	struct fb_data_t *fb_data = (struct fb_data_t *)data;

	//将fb_data的数据写入page
	len = sprintf(page, "%s = %s\n", fb_data->name, fb_data->value);

	return len;
}

static int proc_write_foobar(struct file *file, const char *buffer, unsigned long count, void *data)
{
	int len;
	struct fb_data_t *fb_data = (struct fb_data_t *)data;

	if (count > FOOBAR_LEN)
		len = FOOBAR_LEN;
	else
		len = count;

	//写函数的核心语句,将用户态的buffer写入内核态的value中
	if (copy_from_user(fb_data->value, buffer, len))
		return -EFAULT;

	fb_data->value[len] = '\0';

	return len;
}

当用户读我们刚创建的虚拟文件时,该文件对应的read_proc函数将被调用。该函数将数据写入内核的缓冲区中。上述读函数的例子中,缓冲区即为page。当用户给虚拟文件写数据时,write_proc函数将被调用,该函数从缓冲区buffer中读取count个字节的数据。

3.测试

接下来我们将进行一系列的读写测试。由于我们只为jiffies与其符号链接文件jiffies_too实现了读回调函数,因此它们为只读文件,当对这两个文件进行写操作时就会出现错误;对于foo和bar文件,我们为其实现了读、写函数,因此既可以对它们进行读操作也可以进行写操作。

root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies
jiffies = 833619
root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies_too
jiffies = 834442
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
bar = bar
root@edsionte-desktop:/proc/edsionte_procfs# cat foo
foo = foo
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies
bash: echo: 写操作出错: 输入/输出错误
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies_too
bash: echo: 写操作出错: 输入/输出错误
root@edsionte-desktop:/proc/edsionte_procfs# echo "hello" >> bar
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
bar = hello

示例程序可以在此下载完成代码。

参考:

1.边干边学-Linux内核指导;作者: 李善平;出版社: 浙江大学出版社;

2.使用 /proc 文件系统来访问 Linux 内核的内容;
http://www.ibm.com/developerworks/cn/linux/l-proc.html

QTableWidget基本功能总结

2011年5月7日

QTableWidget类提供了一种基于条目(item)的表格视图模型,在该部件中数据以item为基本单位,每条数据(item)对应一个QTableWidgetItem类的对象,所有数据形成的item组成整个表格。接下来我们创建一个用来显示学生信息的表格,以此为例说明TableWidget的一些常用功能。

1.创建QTableWidget

首先创建studentInfo类,然后在Qt Desinger模式下创建一个QTableWidget部件,对其命名为stuTableWidget。通过在该部件上点击右键创建列项表头,创建完毕后也就同时指定了该表格的列项数。如下:

2.初始化

除了直接在设计模式下创建行数和列数外,我们还可以通过调用相应的方法来设定。比如我们通过setRowCount方法在studentInfo类的构造方法中即可指定行项数。

ui->stuTableWidget->setRowCount(30);

该方法在创建行的同时会自动创建一个用来显示行号的列项,如下:

如果我们的表格只用来显示信息,并不涉及对相应信息的修改,则可以通过下述方法将表格设置为只读模式:

 ui->stuTableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);

setEditTriggers()是QAbstractItemView类中的一个方法,通过向该方法传递相应的参数以设置item的一些属性,比如NoEditTriggers参数可将item设置为只读,DoubleClicked代表双击时item可修改。而QTableWidget继承了QAbstractItemView方法,因此它可以使用该函数。

3.信息显示

表格视图中数据的获取随用途的不同而不同。如果使用于C/S模型的客户端,那么表格中的信息需要从服务器端发送到本地,再相应解析;如果使用在数据库中,则需要从数据库中获取相应信息。这里假定数据已经到达本地,我们通过下面的方法来显示数据信息。

void studentInfo::showInfo()
{
    QTableWidgetItem *tmpItem;

    tmpItem = new QTableWidgetItem(QString("04065061"));
    ui->stuTableWidget->setItem(0, 0, tmpItem);

    tmpItem = new QTableWidgetItem(QString("edsionte"));
    ui->stuTableWidget->setItem(0, 1, tmpItem);

    tmpItem = new QTableWidgetItem(QString("1988.01.28"));
    ui->stuTableWidget->setItem(0, 2, tmpItem);

    tmpItem = new QTableWidgetItem(QString("male"));
    ui->stuTableWidget->setItem(0, 3, tmpItem);

    tmpItem = new QTableWidgetItem(QString("Xi'an Institute of Posts and Telecommunications"));
    ui->stuTableWidget->setItem(0, 4, tmpItem);
}

上述的showInfo方法为第一行设定了相应信息,我们可以看到表格的一行中每个具体的列项都对应一个QTableWidgetItem对象,并通过在setItem方法中指定行号和列号将该item对象设置到表格的具体位置。在上述的showInfo方法中,我们分别通过该方法创建了第一行的第一到第五列的数据(行列下表从0开始)。

4.为表格数据添加右键菜单

有时候我们想通过点击鼠标右键对表格数据进行一些其他操作,比如复制、查看详情等,我们可以按照下面的方法来实现。为了实现点击右键弹出菜单这个功能,我们必须在类studentInfo类中声明一个菜单变量popMenu和一个菜单选项变量action。

class studentInfo : public QMainWindow
{
…………
private:
    Ui::studentInfo *ui;
    QMenu *popMenu;
    QAction *action;

private slots:
    void on_stuTableWidget_customContextMenuRequested(QPoint pos);
…………
};

声明完毕后,我们在studentInfo类的构造函数中对其进行初始化,如下:

    ui->stuTableWidget->setContextMenuPolicy(Qt::CustomContextMenu);
    popMenu = new QMenu(ui->stuTableWidget);
    action = new QAction("Copy", this);

setContextMenuPolicy方法用来设置widget菜单项的显示方法,而CustomContextMenu是唯一与邮件菜单有关的参数,因此这里我们将菜单显示方法设置为该类型。如果widget设置为CustomContextMenu时,当在数据上点击右键时就会发送customContextMenuRequested ( const QPoint & pos )信号,该信号还会捕捉到点击右键的位置,并用pos参数来存储。与此信号关联的槽函数我们定义如下:

void studentInfo::on_stuTableWidget_customContextMenuRequested(QPoint pos)
{
    popMenu->addAction(action);
    popMenu->exec(QCursor::pos());
}

我们首先将菜单选项action添加到邮件弹出菜单popMenu中,再通过exec方法在pos()位置显示该邮件菜单,pos()返回的位置即为点击鼠标的位置。

现在,如果点击右键菜单选项并不会发生任何动作,这是因为我们并没有关联相应的槽函数。由于具体的菜单选项不同,其函数的实现也不同,这里我们只给出框架,如下:

void studentInfo::rightClickedOperation()
{
    //do something
}

定义好槽函数,最关键的是与相应的信号连接。对于上述两个槽函数,我们可以使用两种方法进行信号和槽的关联:在Qt Desinger模式下添加或手动进行connect关联。对于customContextMenuRequested信号,我们使用前种方法实现信号和槽的关联;对于右键菜单选项的功能实现,我们可以通过connect函数实现,如下:

connect(action, SIGNAL(triggered()), this, SLOT(rightClickedOperation()));

分治算法之快速排序

2011年5月4日

快速排序算法也是基于分治思想的一种排序算法,它的基本操作即为比较-交换。

快速排序算法的基本思想是从待排序的序列中选取一个比较标准K(通常选取第一个元素),然后将其余元素依次跟K进行比较。在比较的过程中将大于K的元素移到K的后面,将小于K的元素移到K的前面,最后的结果是将原始序列分为两个子序列,而K元素则恰好位于两个子列中间。上述过程称为一趟快速排序,接下来依次为两个子序列进行快速排序,依次递归。当子序列的长度小于1时,递归停止。此时,原始序列已经成为一个有序的序列了。

根据上面的思想,快速排序算法的代码实现如下所示。quickSort函数对原始序列递归进行快速排序,每次排序时先通过partiton函数得到序列中p到r元素间的分界点q,然后再分别对两个子序列p到q-1和q+1到r进行快速排序。

void quickSort(int *a, int p, int r)
{
	if (p < r) {
		int q = partition(a, p, r);
		quickSort(a, p, q - 1);
		quickSort(a, q + 1, r);
	}
}

partition函数是快速排序算法的关键。该函数选取待排序序列的第一个元素作为基准,通过反复的比较-交换将p到r之间的元素分成两组子序列,一组子序列的元素全部小于x,另一组子序列的元素全部大于x。

在具体的比较-交换过程中,设置两个记录点low和high,并在初始时将基准保存到x中。然后不断进行下面两种扫描:

1.将high从右至左扫描,直到a[high] < x为止,由于此时的a[high]是第一个小于基准x的元素,因此将a[high]和x交换。

2.将low从左至右扫描,直到a[low] >= x为止,由于此时的a[low]是第一个不小于基准x的元素,因此将a[low]和x交换。

当low小于high时会一直持续上述两种扫描,否则称其完成了一次划分过程。每一次的划分过程就会得到分界位置,返回为quickSort函数。

int partition(int *a, int p, int r)
{
	int x, low, high;

	x = a[p];
	low = p;
	high = r;

	while (low < high) {
		while (low < high && a[high] >= x)
			high--;
		if (low < high) {
			a[low] = a[high];
			a[high] = x;
			low++;
		}
		output_data(a, n);

		while (low < high && a[low] < x)
			low++;
		if (low < high) {
			a[high] = a[low];
			a[low] = x;
			high--;
		}
		output_data(a, n);
	}
	a[low] = x;
	return low;
}

在partition函数中,选择第一个元素p作为基准可以保证该函数正常退出。如果选取最后一个元素r作为基准,而该元素又恰好是最大元素,那么partition函数就会返回r,这使得quickSort无限递归下去。完整的代码可在这里下载

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key