Linux上的程序是怎样运行的

发表于 3年以前  | 总阅读数:317 次

如何启动程序

  • 双击(windows系统下),或者在shell终端上执行./a.out
  • 在shell终端上运行可执行程序的标准流程:
  • 启动终端仿真器应用程序
  • 输入可执行文件所在的相对路径或者绝对路径
  • 如果该可执行程序需要输入参数的话,还需要输入参数

比如,我们在终端上输入

ls --version

就会出现如下结果。ps 在此处,我们可以人为ls为可执行程序的名称,--version 是该程序需要的参数。

ls (GNU coreutils) 8.4
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Richard M. Stallman and David MacKenzie.

进入bash: /dev/tty

完整性检查

首先,我们从shell的主函数开始,该函数在shell.c文件中。在主函数执行之前,主要做了以下准备工作:

  • 检查并尝试打开/dev/tty
  • 检查shell是否在调试模式下运行
  • 分析命令行参数
  • 读取shell环境
  • 加载.bashrc、.profile和其他配置文件等。

构建运行环境

在做完上述完整性检查之后,最终会执行reader_loop函数,该函数,定义在eval.c中,主要作用是读取给定的程序名称和参数。然后从execute_cmd.c调用execute_command函数,依次调用以下函数链, 不同的检查,例如我们是否需要启动subshell,是否内置bash函数等等。

reader_loop
-> execute_command
--> execute_command_internal
----> execute_simple_command
------> execute_disk_command
--------> shell_execve

众所周知,Linux的实现语言是c,shell也是其一个应用,也有自己的main函数。进入main函数后,在基本的初始化操作之后,最终进入reader_loop函数。reader_loop会调用execute_command来等待用户输入命令行参数,在用户输入参数之后,将调用execute_command_internal函数。execute_command_internal函数是shell源码中执行命令的实际操作函数。他需要对作为操作参数传入的具体命令结构的value成员进行分析,并针对不同的value类型,再调用具体类型的命令执行函数进行具体命令的解释执行工作。

具体来说:如果value是simple,则直接调用execute_simple_command函数进行执行,execute_simple_command再根据命令是内部命令或磁盘外部命令分别调用execute_builtin和execute_disk_command来执行,其中,execute_disk_command在执行外部命令的时候调用make_child函数fork子进程执行外部命令。

如果value是其他类型,则调用对应类型的函数进行分支控制。举例来说,如果是value是for_commmand,即这是一个for循环控制结构命令,则调用execute_for_command函数。在该函数中,将枚举每一个操作域中的元素,对其再次调用execute_command函数进行分析。即execute_for_command这一类函数实现的是一个命令的展开以及流程控制以及递归调用execute_command的功能。在上述整个调用流程串的最后一步是shell_execve。该函数最终会调用系统函数execve,其声明如下:

int execve(const char *filename, char *const argv [], char *const envp[]);

在该函数中,有三个参数,分别是:

  • filename可执行文件的名称
  • 可执行文件所需的参数
  • 可执行文件所在的环境变量 在该函数中,最终就是运行可执行程序,这一步操作,是在kernel中操作的。

进入内核: execve系统调用

execve系统调用实现

该函数定义在fs/exec.c中,其声明如下:

SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
      return do_execve(getname(filename), argv, envp);
}

execve的实现在这里非常简单,只调用了do_execve函数,其参数为execve的参数。而do_execve函数的定义如下:

int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
    struct user_arg_ptr argv = { .ptr.native = __argv };
    struct user_arg_ptr envp = { .ptr.native = __envp };
    return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

通过上述代码,我们可以看到,在do_execve中,最终调用了do_execveat_common,其除了使用do_execve中的参数之外,还有额外的两个参数。下面是do_execveat_common的具体代码(此处我们去掉了一些不必要放入判断代码)

static int do_execveat_common(int fd, struct filename *filename,
         struct user_arg_ptr argv,
         struct user_arg_ptr envp,
         int flags)
{
 struct linux_binprm *bprm;
 int retval;

 if (IS_ERR(filename))
  return PTR_ERR(filename);

 ...
 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);
 bprm->argc = retval;

 retval = count(envp, MAX_ARG_STRINGS);
 bprm->envc = retval;

 retval = bprm_stack_limits(bprm);

 retval = copy_string_kernel(bprm->filename, bprm);
 bprm->exec = bprm->p;

 retval = copy_strings(bprm->envc, envp, bprm);

 retval = copy_strings(bprm->argc, argv, bprm);

 retval = bprm_execve(bprm, fd, filename, flags);
 putname(filename);
 return retval;
}

安全性检查

第一个参数AT_FDCWD是当前目录的文件描述符,第五个参数是标志。我们稍后会看到。do_execveat_common函数检查文件名指针并返回它是否为NULL。在此之后,它检查当前进程的标志,表明未超出正在运行的进程的限制:

if (IS_ERR(filename))
    return PTR_ERR(filename);

if ((current->flags & PF_NPROC_EXCEEDED) &&
atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
    retval = -EAGAIN;
    goto out_ret;
}

current->flags &= ~PF_NPROC_EXCEEDED;

如果这两项检查成功,我们将在当前进程的标志中取消设置PF_NPROC_EXCEEDED标志,以防止执行程序失败。在下一步中,我们调用在kernel/fork.c中定义的unshare_files函数,并取消共享当前任务的文件,并检查此函数的结果:

retval = unshare_files(&displaced);
if (retval)
    goto out_ret;

调用此函数的目的旨在消除执行二进制文件的文件描述符的潜在泄漏。在下一步中,我们开始准备由struct linux_binprm结构(在include/linux/binfmts.h头文件中定义)表示的bprm。

二进制参数准备

struct linux_binprm

linux_binprm结构用于保存加载二进制文件时使用的参数。例如,它包含vm_area_struct,表示将在给定地址空间中连续间隔内的单个内存区域,将在该空间中加载应用程序。mm字段,它是二进制文件的内存描述符,指向内存顶部的指针以及许多其他不同的字段。

分配内存

在do_execveat_common函数中,执行alloc_bprm函数,最终会调用如下:

bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
    goto out_files;

准备工作

retval = prepare_bprm_creds(bprm);
if (retval)
    goto out_free;

check_unsafe_exec(bprm);
current->in_execve = 1;

初始化linux_binprm中的cred结构变量,该结构变量中包含任务的实际uid,任务的实际guid,虚拟文件系统操作的uid和guid等。然后,对check_unsafe_exec函数的调用将当前进程设置为in_execve状态。

计算命令行参数和环境变量

bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc) < 0)
    goto out;

bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc) < 0)
    goto out;

在上述代码中,MAX_ARG_STRINGS是头文件中定义的上限宏,它表示传递给execve系统调用的最大字符串数。MAX_ARG_STRINGS的值:

`#define MAX_ARG_STRINGS 0x7FFFFFFF`

设置

完成所有这些操作后,我们调用do_open_execat函数,该函数

  • 搜索并打开磁盘上的可执行文件并检查,
  • 从noexec挂载点绕过标志0加载二进制文件(我们需要避免从不包含proc或sysfs等可执行二进制文件的文件系统中执行二进制文件),
  • 初始化文件结构并返回此结构上的指针。接下来,我们可以在此之后看到对sched_exec的调用。sched_exec函数用于确定可以执行新程序的最小负载处理器,并将当前进程迁移到该处理器。

file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
    goto out_unmark;

sched_exec();

之后,我们需要检查给出可执行二进制文件的文件描述符。我们尝试检查二进制文件的名称是否从/符号开始,或者给定的可执行二进制文件的路径是否相对于调用进程的当前工作目录进行了解释,或者文件描述符为AT_FDCWD。如果这些检查之一成功,我们将设置二进制参数文件名:

bprm->file = file;

if (fd == AT_FDCWD || filename->name[0] == '/') {
    bprm->filename = filename->name;
}

否则,如果文件名称为空,则将文件名设置为/dev/fd/%d (即/dev/fd/文件描述符),否则将文件名重新设置为/dev/fd/%d/文件名(其中,fd指向可执行文件的文件描述符)

} else {

    if (filename->name[0] == '\0')
        pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
    else
        pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",    fd, filename->name);

    if (!pathbuf) {
        retval = -ENOMEM;
        goto out_unmark;
    }

    bprm->filename = pathbuf;
}

bprm->interp = bprm->filename;

需要注意的是,我们不仅设置了bprm-> filename,还设置了bprm-> interp,它将包含程序解释器的名称。现在,我们只是在此处写相同的名称,但是稍后将使用程序解释器的真实名称对其进行更新,其具体取决于程序的二进制格式。

准备内存相关信息

retval = bprm_mm_init(bprm);
if (retval)
     goto out_unmark;

其中,bprm_mm_init的定义如下:

static int bprm_mm_init(struct linux_binprm *bprm)
{
 int err;
 struct mm_struct *mm = NULL;

 bprm->mm = mm = mm_alloc();
 err = -ENOMEM;
 if (!mm)
  goto err;

 /* Save current stack limit for all calculations made during exec. */
 task_lock(current->group_leader);
 bprm->rlim_stack = current->signal->rlim[RLIMIT_STACK];
 task_unlock(current->group_leader);

 err = __bprm_mm_init(bprm);
 if (err)
  goto err;

 return 0;

err:
 if (mm) {
  bprm->mm = NULL;
  mmdrop(mm);
 }

 return err;
}

在函数bprm_mm_init中,其功能主要是初始化mm_struct 和 vm_area_struct结构。

读取二进制(ELF)文件

调用prepare_binprm函数将inode的uid填充到linux_binprm结构中,并从二进制可执行文件中读取128个字节。我们只从可执行文件中读取前128个,因为我们需要检查可执行文件的类型。我们将在后续步骤中阅读可执行文件的其余部分。

retval = prepare_binprm(bprm);
if (retval < 0)
    goto out;

准备好linux_bprm结构后,我们通过调用copy_strings_kernel函数将可执行二进制文件的文件名,命令行参数和环境变量从内核复制到linux_bprm:

retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0)
    goto out;

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

retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
    goto out;

并将指针设置为我们在bprm_mm_init函数中设置的新程序堆栈的顶部bprm-> exec = bprm-> p; 堆栈的顶部将包含程序文件名,我们将该文件名存储到linux_bprm结构的exec字段中。

处理参数结构

通过调用exec_binprm函数来存储当前当前任务所在进程的pid

retval = exec_binprm(bprm);
if (retval < 0)
    goto out;

在exec_binprm函数中,也会调用search_binary_handler。当前,Linux内核支持以下二进制格式:

  • binfmt_script: 支持从#!开始的解释脚本。线;
  • binfmt_misc: 根据Linux内核的运行时配置,支持不同的二进制格式;
  • binfmt_elf: 支持elf格式;
  • binfmt_aout: 支持a.out格式;
  • binfmt_flat: 支持平面格式;
  • binfmt_elf_fdpic: 支持elf FDPIC二进制文件;
  • binfmt_em86: 支持在Alpha机器上运行的Intel elf二进制文件。因此,search_binary_handler尝试调用load_binary函数并将linux_binprm传递给该函数。如果二进制处理程序支持给定的可执行文件格式,它将开始准备可执行二进制文件的前期工作。该函数定义如下:
int search_binary_handler(struct linux_binprm *bprm)
{
    ...
    ...
    ...
    list_for_each_entry(fmt, &formats, lh) {
    retval = fmt->load_binary(bprm);

    if (retval < 0 && !bprm->mm) {
        force_sigsegv(SIGSEGV, current);
        return retval;
    }
}

return retval;

在load_binary中检查linux_bprm缓冲区中的魔数(每个elf二进制文件的头中都包含魔数,我们从可执行二进制文件中读取了前128个字节),如果不是elf二进制,则退出。

运行

完整性检测

如果给定的可执行文件为elf格式,则load_elf_binary继续并检查可执行文件的体系结构和类型,并在体系结构错误且可执行文件不可执行,不可共享时退出:

if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
    goto out;
if (!elf_check_arch(&loc->elf_ex))
    goto out;

设置地址空间和依赖

尝试加载描述段的程序头表。从磁盘上读取与我们的可执行二进制文件链接的程序解释器和库,并将其加载到内存中。

elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
if (!elf_phdata)
    goto out;

程序解释器指定在可执行文件的.interp部分(在大多数情况下,对于x86_64,链接器为– /lib64/ld-linux-x86-64.so.2)。它设置堆栈并将elf二进制文件映射到内存中的正确位置,映射了bss和brk部分,并做了许多其他不同的事情来准备要执行的可执行文件。在执行load_elf_binary的最后,我们调用start_thread函数并将三个参数传递给该函数:

    start_thread(regs, elf_entry, bprm->p);
    retval = 0;
out:
    kfree(loc);
out_ret:
    return retval;

这些参数是:

  • 新任务的寄存器集
  • 新任务入口点的地址
  • 新任务的堆栈顶部地址 从函数名称可以理解,它启动了一个新线程,但事实并非如此。start_thread函数只是准备新任务的寄存器以准备运行。下面是定义:
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
    start_thread_common(regs, new_ip, new_sp,
    __USER_CS, __USER_DS, 0);
}

通过上面代码,我们能够看到,在start_thread函数中,最终还是调用了start_thread_common函数。

开始执行

static void
start_thread_common(struct pt_regs *regs, unsigned long new_ip,
unsigned long new_sp,
unsigned int _cs, unsigned int _ss, unsigned int _ds)
{
    loadsegment(fs, 0);
    loadsegment(es, _ds);
    loadsegment(ds, _ds);
    load_gs_index(0);
    regs->ip = new_ip;
    regs->sp = new_sp;
    regs->cs = _cs;
    regs->ss = _ss;
    regs->flags = X86_EFLAGS_IF;
    force_iret();
}

start_thread_common函数将fs段寄存器填充为零,并将es&ds填充数据段寄存器的值。之后,我们将新值设置为指令指针,cs段等。在start_thread_common函数的末尾,我们可以看到force_iret宏,该宏通过iret指令强制返回系统调用。

然后,创建了在用户空间中运行的新线程,随后可以从exec_binprm返回,再次处于do_execveat_common中。exec_binprm完成执行后,释放之前分配的结构的内存,然后返回。

从execve系统调用处理程序返回后,将开始执行程序。之所以可以这样做,是因为之前配置了所有与上下文相关的信息。

如我们所见,execve系统调用不会将控制权返回给进程,但是调用者进程的代码,数据和其他段只是被程序段所覆盖。应用程序的退出将通过退出系统调用实现。

至此,整个程序从开始运行到退出,整个流程完。

END

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/5cMFr7fAUsBPEWNi9bqtvg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237229次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录