在linux中执行命令

10/19/2023

在很多时候我们需要在程序中执行命令,linux提供的方法也很多、但是不够灵活,接下来介绍几种linux执行命令的方法。

# exec

当一个程序开始调用一种exec函数时(为什么要强调一种?因为linux中大致提供了7种exec函数),该进程执行的程序完全替换为新程序,从其main函数开始执行,所以前后的进程ID并未改变,exec只是在磁盘上用新程序替换了当前程序的正文段、数据段、堆、栈。这样就意味着写在exec之后的代码都不会被执行。

具体可以看man page对这个系列函数的解释:

On a successful function call, it does not return anything as the current process gets entirely replaced with the process specified in the function. So, any lines that are written after the `execl()` function would not get executed.
1

这七个函数分别是exec、execl、execv、execle、execve、execlp、execvp、fexecve。这些函数的第一个区别是前四个函数使用路径名作为参数,后两个取文件名作为参数,最后一个指定文件描述符作为参数。如果文件名包含'/',也会视为路径名。可变参数列表的第一个参数是程序名称。

注意:

execl、execle、execlp三个函数签名都是(const char *__file, const char *__arg, ...) 这种格式。这种语法强制说明了最后一个参数必须是一个指针,否则会被解释成整型,如果一个整型数与char*的长度不同,那么exec函数执行的实际参数将会出错。

我们写一个简单程序来具体演示一下:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main()
{
    char *command = "echo \"hello word!\"";
    int status;

    status = execl("/bin/sh", "sh", "-c", command, (void *)0);
    if (status == -1)
    {
        printf("error exec %s\n", strerror(errno));
        return -1;
    }
    printf("after execl\n");
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可以看到输出并没有我们最后打印的数据,所以也就验证了开始我们说的,exec替换了程序的数据。

[root@localhost build]# ./test 
hello word!
[root@localhost build]# 
1
2
3

我们有没有方法进一步验证呢?有的客官,别着急往后看。我们可以写一个新的程序,让这个程序去调用,代码如下:

#被调用程序
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("new programe pid:%d\n", getpid());
    return 0;
}

#调用程序
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main()
{
    char *command = "/bin/print_pid";
    int status;
    printf("current pid:%d\n", getpid());
    status = execl("/bin/sh", "sh", "-c", command, (void *)0);
    if (status == -1)
    {
        printf("error exec %s\n", strerror(errno));
        return -1;
    }
    printf("after execl\n");
    return 0;
}
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

输出:

[root@localhost build]# ./test 
current pid:61735
new programe pid:61735
1
2
3

看吧,在新的程序执行的时候,和调用他的进程进程号是一样的。


不过瘾?我们来点硬核的,我们看一下linux内核是如何写的,前面的具体细节调用我就不说了,execl最后是去调用execve

int execve(const char *filename, char *const argv[], char *const envp[])
{
	int ret = sys_execve(filename, argv, envp);

	if (ret < 0) {
		SET_ERRNO(-ret);
		ret = -1;
	}
	return ret;
}
1
2
3
4
5
6
7
8
9
10

execve又去调用syscall这个函数具体细节就不说了,感兴趣的可以自己去看,因为他里面是内联的汇编代码调用系统调用,展开来说要开始介绍汇编了,而且每个架构的寄存器、中断号还不一样。


static __attribute__((unused))
int sys_execve(const char *filename, char *const argv[], char *const envp[])
{
	return my_syscall3(__NR_execve, filename, argv, envp);
}

1
2
3
4
5
6
7

我们直接跳到最后来看它是如何替换进程空间的,这里面牵扯的知识太多、我只能做一些简单的介绍,写得不好欢迎指正。

在新的程序执行之前会有许多的前置工作,检查当前的进程、用户线程是否超过限制。binprm结构用于保存加载二进制文件时使用的参数。例如,它包含vm_area_struct,表示将在给定地址空间中连续间隔内的单个内存区域,将在该空间中加载应用程序。mm字段,它是二进制文件的内存描述符,指向内存顶部的指针以及许多其他不同的字段。

	if ((current->flags & PF_NPROC_EXCEEDED) &&
	    is_rlimit_overlimit(current_ucounts(), UCOUNT_RLIMIT_NPROC,
				rlimit(RLIMIT_NPROC))) {
		retval = -EAGAIN;
		goto out_ret;
	}

	current->flags &= ~PF_NPROC_EXCEEDED;

	bprm = alloc_bprm(fd, filename);
	if (IS_ERR(bprm)) {
		retval = PTR_ERR(bprm);
		goto out_ret;
	}

	retval = count(argv, MAX_ARG_STRINGS);
	if (retval == 0)
		pr_warn_once(
			"process '%s' launched '%s' with NULL argv: empty string added\n",
			current->comm, bprm->filename);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

然后,调用 bprm_stack_limits(bprm) 来处理新程序的栈限制。这可能包括调整栈大小或其他栈相关的设置。如果在这个过程中发生错误(retval < 0),则会跳转到 out_free 标签,进行资源释放。包括将用户态的参数拷贝到内核空间:

	retval = bprm_stack_limits(bprm);
	if (retval < 0)
		goto out_free;

	retval = copy_string_kernel(bprm->filename, bprm);
	if (retval < 0)
		goto out_free;
	bprm->exec = bprm->p;

	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out_free;

	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out_free;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后调用bprm_execve(bprm, fd, filename, flags);函数来继续执行。

未完待续。。。。。。

嘉宾
路文飞