Linux中的二进制可执行文件和脚本可执行文件及Shebang

二进制可执行文件

我们知道,一个C程序经过预处理、编译、汇编、链接就会得到一个二进制可执行文件,这种文件在Linux中叫做ELF文件。比如我们有一个C源代码hello.c

#include <stdio.h>

int main(int argc, char** argv){
  	printf("Hello !\n");
}

我们编译得到 hello 文件,并用file命令可以查看到生成的二进制可执行文件的信息:

gcc hello.c -o hello
file hello
# 输出:
# hello: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=cf2738fd1715f096d4b0e0e4b264146b78b454b1, not strippe

确实是ELF文件,我们可以直接执行它:

./hello
# 输出:
# Hello

这是我们常见的,可以理解的,链接后的可执行文件就是可以直接运行,就像我们在Windows上双击打开一个exe文件那样自然。那么,脚本可执行文件又是怎么一回事呢?

脚本可执行文件及Shebang

脚本可执行文件也可以像运行二进制可执行文件那样来直接运行它。我们知道shell、python等属于脚本语言。令我们好奇的是,脚本程序如 train.py 等看上去只是一个文本文件,为什么也能直接被执行呢 ?

我们知道,想要运行一个脚本文件,我们需要指定一个解释器。通常,我们有两种方式来指定脚本文件的解释器:

  1. 在命令行中指定,如bash run.shpython train.py等。
  2. 通过文件中的第一行Shebang指定。在脚本文件的头上,通常会有一行Shebang:#!。比如:#!/bin/bash#!/home/song/bin/python等。

Shebang通常出现在类Unix系统的脚本中第一行,作为前两个字符。在Shebang之后,可以有一个或数个空白字符,后接解释器的绝对路径用于指明执行这个脚本文件的解释器。在直接调用脚本时,系统的程序载入器会分析 Shebang 后的内容,将这些内容作为解释器指令,并调用该指令,将载有 Shebang 的文件路径作为该解释器的参数,执行脚本,从而使得脚本文件的调用方式与普通的可执行文件类似。例如,以指令#!/bin/sh开头的文件,在执行时会实际调用 /bin/sh 程序(通常是 Bourne shell 或兼容的 shell,例如 bash、dash 等)来执行。

由于 # 符号在许多脚本语言中都是注释标识符,这既是偶然,也是必然。Shebang 的内容会被这些脚本解释器自动忽略。 在 # 字符不是注释标识符的语言中,例如 Scheme,解释器也可能忽略以 #! 开头的首行内容,以提供与 Shebang 的兼容性。

实际上,#!两个字符的ASCII码是两个magic字符,当类UNIX操作系统看到一个文件以这两个字符开头,会将这个文件当做是可执行文件,并且按照其后的解释器来执行它(需要有执行权限)。这时,操作系统实际上加载的是 #! 后面跟的那个二进制文件(即解释器),然后将脚本文件的文本内容作为参数传给这个二进制文件。这一点可以通过观察脚本可执行文件运行使得strace结果中的execve来验证。

Shebang的一些具体用法和注意事项:

  1. 如果脚本文件中没有#!这一行,那么执行时会默认采用当前Shell去解释这个脚本(即:SHELL环境变量)。
  2. 如果#!之后的解释程序是一个可执行文件,那么执行这个脚本时,它就会把文件名及其参数一起作为参数传给那个解释程序去执行。
  3. 如果#!指定的解释程序没有可执行权限,则会报错 bad interpreter: Permission denied。如果#!指定的解释程序不是一个可执行文件,那么指定的解释程序会被忽略,转而交给当前的SHELL去执行这个脚本。
  4. 如果#!指定的解释程序不存在,那么会报错 bad interpreter: No such file or directory。注意:#!之后的解释程序,需要写其绝对路径(如:#!/bin/bash),它是不会自动到环境变量PATH中寻找解释器的。要用绝对路径是因为它会调用系统调用execve,这可以用strace工具来查看。
  5. 脚本文件必须拥有可执行权限。可通过chmod +x [filename] 来添加可执行权限。
  6. 当然,如果你使用类似于 bash test.shpython train.py这样的命令来执行脚本,那么#!这一行将会被忽略掉,解释器当然是用命令行中显式指定的解释器。

我们来试一下,先创建一个py文件world.py,并直接写入为:

print('World !')

我们可以通过在命令行指定python解释器来运行,就像我们一直做的那样:

python world.py
# 输出:
# World !

但是,当我们想像运行二进制可执行文件那样来运行它

./world.py
# 输出:
# -bash: ./world.py: Permission denied

首先会受到一条没有执行权限的命令,如上。这很正常,因为我们创建的时候它是一个文本文件嘛。我们通过 chmod 来使得它可执行,并再次尝试运行它:

chmod +x world.py
./world.py
# 输出:
# ./world.py: line 1: syntax error near unexpected token `'World !''
# ./world.py: line 1: `print('World !')'

问题出现了,和我们之前讨论的一样,由于我们没有通过Shebang来指定脚本的解释器,系统默认用了Shell来解释,那我们的python语法自然是不对的。那这时,想要像运行二进制可执行文件那样去运行它,必须请出我们的Shebang来帮忙在文件内指明解释器的绝对路径。更改world.py 为:

#!/home/song/anaconda3/envs/JJ_env/bin/python
print("World")

这时我们再来运行:

./world.py
# 输出:
# World !

就可以了。我们还可以通过 file 命令再来看一下 world.py 的文件信息:

file world.py
# 输出:
# world.py: a /home/song/anaconda3/envs/JJ_env/bin/python script, ASCII text executable

我们看到该文件是一个ASCII text executable,即 ”文本可执行文件“。不同于ELF二进制可执行文件,但也是可执行文件。也就是说,在Linux的世界中,可执行文件不只有ELF一种。

另外,由于Linux系统对后缀名并不严格要求,我们可以直接将world.py改为world,这样也是可以的,然后就可以通过将world这个脚本可执行文件放到PATH环境变量下,从而将world直接作为一个命令来使用啦!具体可参考笔者另一篇介绍Linux常用环境变量的博客。

总结

总结一下:Linux中除了ELF二进制可执行文件之外,还有脚本可执行文件,要想让脚本可执行文件直接像二进制可执行文件一样运行,而不需在命令行中指定解释器,需要在脚本文件头通过Shebang !#来指定解释器的绝对路径。Shebang的一些具体的注意事项在上文中已经指出。另外,通过将可执行文件(二进制、脚本都可)添加到PATH环境变量的可执行文件搜索目录下,可将在命令行中通过命令来直接使用这些可执行文件。

Ref:

https://blog.csdn.net/u012294618/article/details/78427864

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐