写着写着就从执行命令到内核execve源码分析了

本文详细介绍了Linux中的exec系列函数,如execl、execle等,它们如何替换进程并执行新程序。还探讨了内核层面的实现过程,涉及binprm结构、栈限制调整等内容。
摘要由CSDN通过智能技术生成
在很多时候我们需要在程序中执行命令,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.==

这七个函数分别是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;
}

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

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

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

#被调用程序
#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;
}

输出:

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

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


不过瘾?我们来点硬核的,我们看一下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;
}

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

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

在新的程序执行之前会有许多的前置工作,检查当前的进程、用户线程是否超过限制。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);

然后,调用 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;

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

未完待续。。。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值