转自:http://edsionte.com/techblog/archives/category/linux%E5%86%85%E6%A0%B8%E7%BC%96%E7%A8%8B
背景
如何在Linux内核中执行某些用户态程序或系统命令?在用户态中,可以通过execve()实现;在内核态,则可以通过call_usermodehelpere()实现该功能。如果您查阅了call_usermodehelper()内核函数的源码实现,就可以发现该函数最终会执行do_execve()。而execve系统调用在经历内核的系统调用流程后,也会最终调用do_execve()。
使用举例
1.无输出的可执行文件测试
加载函数demo如下所示:
1 | static int __init call_usermodehelper_init( void ) |
2 | { |
3 | int ret = -1; |
4 | char path[] = "/bin/mkdir" ; |
5 | char *argv[] = {path, "-p" , "/home/tester/new/new_dir" , NULL}; |
6 |
7 | printk( "call_usermodehelper module is starting..!\n" ); |
8 | ret = call_usermodehelper(path, argv, envp, UMH_WAIT_PROC); |
9 | printk( "ret=%d\n" , ret); |
10 | return 0; |
11 | } |
卸载函数demo如下所示:
1 | static void __exit call_usermodehelper_exit( void ) |
2 | { |
3 | int ret = -1; |
4 | char path[] = "/bin/rm" ; |
5 | char *argv[] = {path, "-r" , "/home/tester/new" , NULL}; |
6 | char *envp[] = {NULL}; |
7 |
8 | printk( "call_usermodehelper module is starting..!\n" ); |
9 | ret = call_usermodehelper(path, argv, envp, UMH_WAIT_PROC); |
10 | printk( "ret=%d\n" , ret); |
11 | } |
2.有输出的可执行文件测试
如果该可执行文件有输出,则可以利用输出重定向,不过此时的可执行文件应该是/bin/bash,而实际的可执行文件则称为bash的参数。比如如果想在内核执行ls -la命令,并且将其输出重定向到ls_output中,则在上述的argv[]={“/bin/bash”, “-c”, “ls”, “-la”, “>”, “/home/tester/ls_output”, NULL};
本文虽然说明的是在内核态如何调用用户态程序,不过可以将这种方法抽象一下,看作是内核态主动向用户态发起通信的一种方式。
Linux内核中通过文件描述符获取绝对路径
2014年3月19日背景
在Linux内核中,已知一个进程的pid和其打开文件的文件描述符fd,如何获取该文件的绝对路径?基本思路是先获取该文件在内核中的file结构体,再通过d_path()获取到整个文件的绝对路径。
方法一
如果理解了进程和文件系统数据结构之间的关系,那么这种方法可以采用。基本的方法如下:
1.通过进程pid获取进程描述符task_struct;
2.通过task_struct获取该进程打开文件结构files_struct,从而获取文件描述符表;
3.以fd为索引在文件描述符表中获取对应文件的结构体file;
4.通过file获取对应path结构,该结构封装当前文件对应的dentry和挂载点;
5.通过内核函数d_path()获取该文件的绝对路径;
通过进程pid获取进程描述符demo:
1 | struct task_struct *get_proc(pid_t pid) |
2 | { |
3 | struct pid *pid_struct = NULL; |
4 | struct task_struct *mytask = NULL; |
5 |
6 | pid_struct = find_get_pid(pid); |
7 | if (!pid_struct) |
8 | return NULL; |
9 | mytask = pid_task(pid_struct, PIDTYPE_PID); |
10 | return mytask; |
11 | } |
通过fd以及d_path()获取绝对路径demo:
1 | int get_path( struct task_struct *mytask, int fd) |
2 | { |
3 | struct file *myfile = NULL; |
4 | struct files_struct *files = NULL; |
5 | char path[100] = { '\0' }; |
6 | char *ppath = path; |
7 |
8 | files = mytask->files; |
9 | if (!files) { |
10 | printk( "files is null..\n" ); |
11 | return -1; |
12 | } |
13 | myfile = files->fdt->fd[fd]; |
14 | if (!myfile) { |
15 | printk( "myfile is null..\n" ); |
16 | return -1; |
17 | } |
18 | ppath = d_path(&(myfile->f_path), ppath, 100); |
19 |
20 | printk( "path:%s\n" , ppath); |
21 | return 0; |
22 | } |
从上面的代码可以看出,从fd到file结构的获取均通过各个数据结构之间的指向关系获取。
方法二
与方法一的思路相同,但是可以直接使用内核提供的函数fget()进行fd到file的获取。这种方法使用比较简单,程序更加安全,不过就是少了对数据结构关系的思考过程。其实也可以将fget()函数的实现过程作为参考,欣赏内核中代码实现的严谨性。
libc库和系统调用
2012年6月2日Linux系统调用这部分经常出现两个词:libc库和封装函数,不知道你是否清楚它们的含义?
libc
libc是Standard C library的简称,它是符合ANSI C标准的一个标准函数库。libc库提供C语言中所使用的宏,类型定义,字符串操作函数,数学计算函数以及输入输出函数等。正如ANSI C是C语言的标准一样,libc只是一种函数库标准,每个操作系统都会按照该标准对标准库进行具体实现。通常我们所说的libc是特指某个操作系统的标准库,比如我们在Linux操作系统下所说的libc即glibc。glibc是类Unix操作系统中使用最广泛的libc库,它的全称是GNU C Library。
类Unix操作系统通常将libc库作为操作系统的一部分,它被视为操作系统与用户程序之间的接口。libc库不仅实现标准C语言中的函数,而且也包含自己所属的函数接口。比如在glibc库中,既包含标准C中的fopen(),又包含类Unix系统中的open()。在类Unix操作系统中,如果缺失了标准库,那么整个操作系统将不能正常运转。
与类Unix操作系统不同的是,Windows系统并不将libc库作为整个核心操作系统的一部分。通常每个编译器都附属自己的libc库,这些libc既可以静态编译到程序中,又可以动态编译到程序中。也就是说应用程序依赖编译器而不是操作系统。
封装函数
在Linux系统中,glibc库中包含许多API,大多数API都对应一个系统调用,比如应用程序中使用的接口open()就对应同名的系统调用open()。在glibc库中通过封装例程(Wrapper Routine)将API和系统调用关联起来。API是头文件中所定义的函数接口,而位于glibc中的封装例程则是对该API对应功能的具体实现。事实上,我们知道接口open()所要完成的功能是通过系统调用open()完成的,因此封装例程要做的工作就是先将接口open()中的参数复制到相应寄存器中,然后引发一个异常,从而系统进入内核去执行sys_open(),最后当系统调用执行完毕后,封装例程还要将错误码返回到应用程序中。
需要注意的是,函数库中的API和系统调用并没有一一对应的关系。应用程序借助系统调用可以获得内核所提供的服务,像字符串操作这样的函数并不需要借助内核来实现,因此也就不必与某个系统调用关联。
不过,我们并不是必须通过封装例程才能使用系统调用,syscall()和_syscallx()两个函数可以直接调用系统调用。具体使用方法man手册中已经说明的很清楚了。
参考:
1. http://en.wikipedia.org/wiki/Libc
2. man syscalls