当我向 shell 提供“命令”而不是脚本时会发生什么?

当我向 shell 提供“命令”而不是脚本时会发生什么?

这个问题的一个变体已被问过几次(这里这里,例如),但我担心答案要么没有完全抓住我的问题,要么假设的内容可能比我知道的更多。

我将通过示例提出我的问题,但粗略地说,我想要理解的是(1)如何shell 能够识别可执行文件和脚本,如果能够识别,则 (2) 识别后接下来发生的情况是否存在任何差异。

假设我的工作目录中有一个 shell 脚本script和一个可执行文件(我将“可执行文件”理解为二进制的“机器代码”,但可能并不正确)exe。假设我正在与 bash shell 交互,并且假设scriptbash 和 tcsh 都可以运行它。此外,假设第一行才不是从 Shebang 开始#!...

  1. 假设我script在命令行提示符下输入。 bash 如何确定这是一个脚本而不是可执行文件,一旦确定它会做什么? (我认为答案是“创建一个新进程,特别是一个新的 shell(即子 shell),在本例中是 bash(因为我没有 shebang),然后执行其中脚本中的命令”,但是我不知道。)

  2. 现在假设我exe在命令行提示符下输入。 bash 如何确定这是一个可执行文件而不是一个脚本?一旦确定它会做什么? (我认为答案是“创建一个新进程并等待可执行文件完成”,但我不确定。)

  3. 现在假设我修改script为包含#! /bin/tcsh在第一行中。 bash 如何确定这是一个脚本(shebang 是否会更改 (1) 的答案中的任何内容?)而不是可执行文件,一旦确定它会做什么? (我认为答案是“创建一个新进程,特别是一个新的 shell(即子 shell),在本例中是 tcsh(因为我是 shebang),然后执行其中脚本中的命令”,但我我不确定。)

答案1

首先,是执行命令的系统。

有了这个外壳代码:

cmd and its args

shell 在列出的目录中查找cmd别名、函数、内置函数和可执行文件(您有权执行的常规文件)$PATH(不是当前工作目录,除非它恰好位于其中,$PATH这是不好的做法)。

如果是后者,它execve()通常在子进程中使用 3 个参数调用系统调用:

  1. 文件的路径 ( /path/to/cmd)
  2. 参数列表 ( ["cmd", "and", "its", "args", 0])
  3. var=value每个导出变量的字符串列表。

如果execve()根据文件类型处理文件:

  • 如果是 ELF 二进制可执行文件,它将在内存中加载/映射它(或其部分)以及可能的动态链接器,该链接器将负责加载更多共享库......并开始运行它。
  • 如果它以 开头#!,它(而不是 shell)会将行的其余部分解释为要执行的另一个文件,并将文件的路径作为额外参数传递给该命令。 (如果是#! foo bar,它将执行与 相同的操作execve("foo", ["foo" or "cmd", "bar", "/path/to/cmd", "and", "its", "args"], env))。
  • 除了 ELF 之外,系统还可以支持许多不同的本机可执行文件格式,并且某些系统还可以配置为将解释器与匹配文件开头的模式关联起来(请参阅 Linux 上的 binfmt_misc)。

进程内存将在进程中大部分被擦除,并且execve()永远不会返回,因为进程现在正在运行可执行文件中的代码。

如果无法识别文件格式,execve()将返回-1(指示失败)并ENOEXEC作为错误代码。

在这种情况下,需要 POSIX shell,以及execlp()C 函数(不是系统调用)或env/ find -exec...(或者通常所有用于在 POSIX 工具箱中执行命令的东西)之类的东西来将其视为脚本sh(尽管可能检查后,它看起来像是使用一些启发式方法来避免运行sh一些随机的东西)。

大多数 shell 通过执行它来实现sh这一点,就好像有一个#! /path/to/the/standard/sh -shebang 一样。有些是 POSIX 兼容的sh实现或具有 POSIX sh 模式,它通过在子进程中解释文件本身来实现。

所以对于你的三种情况:

  1. shebang-less script,运行 with./script而不是scriptwhich 可能会运行/usr/bin/script:shellexecve("./script", ["./script", 0], env)在子进程中运行,并execve()因 ENOEXEC 失败,因为它不是已知的格式,因此 shell 将(可选)查看它,看看它看起来像它可以在sh语法中运行execve("/bin/sh", ["sh", "-", "./script"])(带有argv[0]、 或 的变体-)或解释它本身(并且父 shell 进程等待其终止)。
  2. ./exe:shellexecve("./exe", ["./exe", 0], env)在子进程中执行此操作,子进程成功,父进程等待其终止。
  3. ./script使用#! /bin/tcshshebang:shellexecve("./script", ["./script", 0], env)也会成功,因为系统将其识别为脚本。execve()将依次/bin/tcsh./script参数运行。父 shell 像往常一样等待它的子 shell。

答案2

当你向 shell 发出如下命令时

alpha beta gamma

在命令行完成所有扩展之后,shell 必须确定命令alpha是什么类型。如果它不是别名或定义的函数,则它必须是外部命令。在这种情况下,它将在 指示的目录中搜索该程序PATH,如果找到它,它将使用操作系统(fork + exec)执行它。

脚本不必以一行开头#!。如果文件是可执行的,并且没有标头指示它属于特定类型,则将使用 shell 对其进行处理。

在 GNU/Linux 中,execve系统调用在该文件上失败:它没有可识别的格式。该库execve使用 shell透明地重试另一个。这是strace发生的情况的快照:

32056 execve("./command", ["./command"], 0xbfb71504 /* 27 vars */) = -1 ENOEXEC (Exec format error)
32056 execve("/bin/sh", ["/bin/sh", "./command"], 0xbfb71504 /* 27 vars */) = 0

这里./command是一个文件,echo foo其中包含唯一一行。它具有执行权限。

被跟踪的程序认为它只是对该execvp函数进行了一次调用。该函数在内部调用execve内核系统调用。第一次调用它无法直接执行脚本,因此execvp再次尝试,这次使用/bin/sh可执行文件作为参数零。

这实际上不只是 Linux 的事情;这也是 Linux 的事情。这是 POSIX 所要求的,它说:

一种常见的历史实现是,execl()、execv()、execle() 和 execve() 函数对于任何无法识别为可执行文件(包括 shell 脚本)的文件返回 [ENOEXEC] 错误。当 execlp() 和 execvp() 函数遇到此类文件时,它们假定该文件是 shell 脚本,并调用已知的命令解释器来解释此类文件。现在 POSIX 要求这样做。

因此,没有一行的 shell 脚本的调度#!对于使用p-suffixed ( PATH-searching)exec函数的应用程序是透明的。你的 shell 要么这样做,要么自己实现类似的逻辑。 (也就是说,shell 可能正在实现自己的PATH搜索,然后使用execve, 并以类似的方式从失败中恢复。)

相关内容