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
等看上去只是一个文本文件,为什么也能直接被执行呢 ?
我们知道,想要运行一个脚本文件,我们需要指定一个解释器。通常,我们有两种方式来指定脚本文件的解释器:
- 在命令行中指定,如
bash run.sh
,python train.py
等。 - 通过文件中的第一行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的一些具体用法和注意事项:
- 如果脚本文件中没有#!这一行,那么执行时会默认采用当前Shell去解释这个脚本(即:SHELL环境变量)。
- 如果#!之后的解释程序是一个可执行文件,那么执行这个脚本时,它就会把文件名及其参数一起作为参数传给那个解释程序去执行。
- 如果#!指定的解释程序没有可执行权限,则会报错
bad interpreter: Permission denied
。如果#!指定的解释程序不是一个可执行文件,那么指定的解释程序会被忽略,转而交给当前的SHELL去执行这个脚本。 - 如果#!指定的解释程序不存在,那么会报错
bad interpreter: No such file or directory
。注意:#!之后的解释程序,需要写其绝对路径(如:#!/bin/bash
),它是不会自动到环境变量PATH
中寻找解释器的。要用绝对路径是因为它会调用系统调用execve
,这可以用strace工具来查看。 - 脚本文件必须拥有可执行权限。可通过
chmod +x [filename]
来添加可执行权限。 - 当然,如果你使用类似于
bash test.sh
,python 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