ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux可执行文件从入门到排查:ELF格式、权限与PATH机制

Linux可执行文件从入门到排查:ELF格式、权限与PATH机制 聊 Linux 绕不开一个概念可执行文件。很多刚入门的朋友第一次拿到一个“绿色”的文件在图形界面里双击没反应跑去终端敲./xxx结果弹出来一句Permission denied当场就懵了。这篇文章想把这件事彻底讲透Linux 下的可执行文件到底是什么和 Windows 的 exe 有什么不一样为什么有时候明明看着有执行权限还是跑不起来以及日常遇到报错时怎么排查。内容主要面向刚开始学 Linux 常用命令的入门用户也适合运维、嵌入式开发的朋友查漏补缺毕竟这类细节在 linux 面试题和实际生产环境里出现的频率都不低。1. 可执行文件到底是什么ELF 格式与权限位很多人以为“可执行文件”是一个固定的后缀名像 Windows 的.exe那样。但在 Linux 里完全不是这个逻辑。一个文件能不能执行不取决于后缀而是取决于两件事文件内容是否符合系统能够识别的可执行格式以及文件权限位上有没有“执行”这一项。这两件事缺一不可。1.1 ELF 格式Linux 可执行文件的“身份证”Linux 下最常见的可执行文件格式叫 ELF全称 Executable and Linkable Format可执行与可链接格式。.exe在 Windows 对应的是 PE 格式而 ELF 是 Linux 世界里“双击就能跑”的基础。打开终端随手敲一句file /bin/ls大概率会看到类似这样的输出$ file /bin/ls /bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]..., for GNU/Linux 3.2.0从这段输出里能读出很多信息架构是 x86-64是 PIE 类型的动态链接可执行文件解释器路径在/lib64/ld-linux-x86-64.so.2。如果你在嵌入式开发里拿到一个 ARM 平台编译出来的可执行文件file命令会显示ARM aarch64这时候放到 x86 服务器上执行系统会直接报Exec format error因为 CPU 指令集根本不认识。想看得更细可以用readelf -h查看 ELF 头重点关注Type字段。常见的有三类类型含义常见场景ET_REL可重定位文件编译产生的.o目标文件ET_EXEC静态可执行文件少数静态编译的二进制ET_DYN动态可执行文件/共享库绝大多数 Linux 可执行文件和.so动态库现在的 Linux 发行版里大多数可执行文件都是 ET_DYN 类型也就是 PIE位置无关可执行文件。这个设计主要是出于安全考虑地址随机化ASLR可以更好地发挥作用攻击者不容易猜中程序在内存里的加载地址。1.2 权限位才是真正的“执行开关”格式是“能不能被内核识别”的前提但真正决定你能不能运行它的是文件权限位。Linux 的权限模型用一个九位字符串表示比如-rwxr-xr-x。拆开看就是三组三位的组合第一组三位文件所有者user的权限简称 u第二组三位文件所属组group的权限简称 g第三组三位其他用户other的权限简称 o每一位上r代表读readw代表写writex代表执行execute。没有对应权限时这一位是-。chmod 755等价于rwxr-xr-x数字对应关系是读 4、写 2、执行 17 4215 41。这里有个新手最容易忽略的知识点目录的可执行权限和文件的可执行权限含义不同。文件上的x表示“可以被内核加载运行”目录上的x表示“可以进入这个目录”也就是可以cd进去。如果目录只有r没有x你能看到目录里的文件名列表但无法进入目录也无法访问里面的任何文件。所以才说 Linux 下一切都是文件但目录的执行权限更像是一把“进门钥匙”。实际操作中还有一个很常见的误区用useradd新建用户之后发现新用户不能运行某个程序。这时候先别急着怀疑环境变量先用ls -l看下这个文件对 othero有没有执行权限。如果o位是-那其他用户自然没有权限运行得用chmod ox或者把用户加进文件所属组。2. 从源码到可执行文件三种常见生成路径可执行文件一般有三种来源编译型语言编译出来的二进制、脚本类文件加了解释器声明后“变得可执行”、以及把动态库链接进二进制的静态编译产物。很多人分不清这几种场景的差异遇到问题就容易卡壳。2.1 编译型可执行文件gcc 一条命令的背后最简单的例子写一个 C 程序然后编译出可执行文件。// hello.c #include stdio.h int main() { printf(hello, linux executable\n); return 0; }编译命令只有一行gcc hello.c -o hello但这一行命令背后其实做了四件事预处理展开头文件和宏、编译生成汇编代码、汇编生成机器码目标文件、链接把目标文件和库文件合并成最终可执行文件。其中链接环节又分动态链接和静态链接这个后面再细说。编译完成后用file hello查看会看到 ELF 格式标记。直接运行./hello屏幕上会打印hello, linux executable。这时如果你故意把执行权限去掉chmod -x hello ./hello系统会回答Permission denied。但注意文件内容没有任何变化它依然是一个合法的 ELF 文件只是你不满足“权限位上有 x”这个前置条件。反之如果你chmod x hello恢复权限它又能跑了。这说明内核execve系统调用在启动一个程序时会先检查权限位再检查文件格式两道关卡缺一不可。2.2 脚本类可执行文件chmod x 与 shebang 的配合脚本文件不是 ELF 格式为什么也能像命令一样直接执行关键在于脚本第一行有一个特殊的“魔法声明”叫 shebang格式是#!加解释器路径。比如#!/bin/bash echo run script把这个内容保存成test.sh然后chmod x test.sh再执行./test.sh内核看到这个文件的格式不是 ELF就会去读第一行的 shebang找到/bin/bash然后调用 Bash 来解析这个脚本。所以脚本文件的“可执行”其实是两层含义权限位有x同时解释器存在。这里有不少有趣的小知识点。比如你把test.sh的x权限去掉然后用bash test.sh运行依然可以成功。因为bash本身是一个可执行文件它把test.sh当成参数读进来解析test.sh自己有没有x权限就不重要了。这也是新手常犯糊涂的地方为什么./script.sh报 Permission denied但bash script.sh又能跑原因就在这。还有个经典坑在 Windows 上编辑过的脚本传到 Linux 里直接执行经常报类似这种错误/usr/bin/env: bash\r: No such file or directory原因是 Windows 的换行符是\r\n而 Linux 只认\n。\r被当成解释器路径的一部分于是系统去找一个名叫bash\r的“解释器”当然找不到。处理办法也简单sed -i s/\r$// test.sh或者安装dos2unix工具一键转换。很多“Linux 脚本在服务器上跑不起来”的求助帖最后都是这个问题。2.3 动态链接还是静态链接ldd 与库依赖编译型可执行文件还有一个绕不开的话题动态链接和静态链接。默认情况下gcc hello.c -o hello产生的是动态链接的可执行文件运行的时候需要依赖系统的共享库比如libc.so.6。用ldd命令可以看到它依赖哪些库$ ldd hello linux-vdso.so.1 (0x00007ffe1b3e0000) libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...) /lib64/ld-linux-x86-64.so.2 (0x00007f...)动态链接的好处是节省磁盘和内存空间公共库在系统里只留一份多个程序共享这也是绝大部分 Linux 软件采用的方式。静态链接则是把程序用到的库代码直接编译进二进制里体积会大不少但运行时不再依赖外部库非常适合拷贝到容器、嵌入式系统或者没有库环境的机器上使用gcc -static hello.c -o hello_static静态编译后的文件体积可以从十几 KB 涨到几百 KB 甚至上 MB这就是“把整套图书馆复印了一份带在身上”的代价。而动态链接更像借书前提是图书馆系统库必须存在。判断一个不知名二进制是不是缺库标准流程就是ldd。缺库时的报错一般长这样./hello: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory这种问题不一定要往系统目录里硬塞库可以先看库在哪个目录然后用LD_LIBRARY_PATH临时指定export LD_LIBRARY_PATH/opt/mylib:$LD_LIBRARY_PATH ./hello当然这只是临时方案正式部署还是应该把库放到/usr/lib或用ldconfig配置好。另外提醒一句编译完的二进制别裸拷贝到其它机器上就跑先ldd确认目标机器上有所有依赖库否则线上踩坑是分分钟的事。3. 命令行执行机制PATH、./ 与命令查找可执行文件准备好了接下来就是怎么让 shell 找到它。很多人敲命令敲习惯了从来没想过一个问题为什么敲ls能直接运行而敲自己刚编译出来的hello却提示command not found必须敲./hello才行这背后的核心机制就是 PATH 环境变量。3.1 为什么要加 ./PATH 环境变量的设计逻辑在 shell 里敲一个命令shell 会按照 PATH 环境变量里列的目录从左到右依次搜索。比如echo $PATH会输出类似/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin这些目录都是系统预先规定好的“命令存放处”。你敲lsshell 依次去这些目录里找最后在/usr/bin/ls找到了于是执行它。但注意这里面没有当前目录也就是.。所以如果你在某个目录下编译出了hello不写路径直接敲helloshell 在所有 PATH 目录里都找不到这个名字于是报command not found。加上./就是明确告诉 shell“在当前目录下找 hello 并执行”。这实际上是现代系统刻意做出的安全设计。如果 PATH 包含当前目录那么当你在一个陌生目录里敲lsshell 有可能优先执行这个目录下的ls而不是系统真正的/usr/bin/ls。如果这个目录是攻击者准备好的里面放了一个同名恶意程序后果很严重。所以 Linux 默认不让.进 PATH不是为了刁难新手而是为了防钓鱼。如果你确实想让自己编译的程序随时能执行正确做法不是把.加进 PATH而是把自己的可执行文件目录加进去。比如把/opt/mybin加入 PATHexport PATH/opt/mybin:$PATH如果想永久生效把这一行写进~/.bashrc然后source ~/.bashrc。注意把新目录放在前面还是后面是有讲究的放在前面优先级更高放在后面系统命令优先。3.2 命令缓存与排查which、type、hash -rPATH 搞清楚了还有一个容易踩的坑命令的缓存机制。同一个 shell 会话里你第一次执行某个命令后shell 会把找到的完整路径记在一个哈希表里下次再用这个命令它就不会再去 PATH 里从头找了。这样能加快命令启动速度。但副作用来了如果你把一个可执行文件从/usr/local/bin挪到了/opt/bin或者重新编译后覆盖了旧版本当前 shell 可能还在用缓存里的旧路径导致你感觉“明明文件已经更新了怎么运行结果还是旧的”。这时候可以用hash -r清空哈希表让 shell 重新按 PATH 搜索。排查“命令找不到”时一个很顺手的组合是which 命令名 type 命令名 command -v 命令名which显示命令在 PATH 中的路径type还能额外告诉你这个命令是不是 shell 内建命令、别名或函数command -v则是一个更符合脚本规范的选择。如果which有输出但执行报错多半是文件权限、架构不匹配或动态库缺失而不是 PATH 的问题。这一套排查路数在 linux 常用命令里属于高频题面试里也经常拿出来问值得记牢。4. 常见执行问题与特殊权限位避坑可执行文件相关的报错翻来覆去就那么几类但每类背后对应的原因不太一样。这里整理一个高频报错速查表基本覆盖了日常工作里的绝大多数场景。另外Linux 文件权限可不止rwx这九位还有三个特殊权限位理解了它们你对“可执行文件能不能跑、跑起来之后是谁的权限”这事的理解会上一个台阶。4.1 高频报错速查表报错信息可能原因处理方式Permission denied文件没有执行权限chmod x 文件名command not found命令不在 PATH 中或压根没安装用绝对路径执行或安装软件或修改 PATHNo such file or directory脚本 shebang 里的解释器路径不存在或二进制架构不匹配检查脚本第一行解释器路径用file确认架构Exec format error文件不是当前内核可识别的格式/架构file查看文件真实类型确认架构一致Text file busy二进制正在运行时被覆盖停止进程后再替换文件或者直接覆盖到新路径/usr/bin/env: bash\rWindows 换行符导致解释器路径带\rsed -i s/\r$// script.sh解压出来的文件名乱码ZIP 包使用中文编码如 GBK而系统默认 UTF-8unzip -O GBK 包名.zip前两个错误是新手最常遇到的也是面试里最基础的两个问题。Text file busy很多人没遇到过但一旦遇到会特别困惑你正在运行某个程序然后去覆盖它的二进制文件系统会告诉你“文件正忙”。这是因为内核不允许直接覆盖正在执行的二进制文件。解决办法是先停止相关进程再执行更新操作或者把新文件写到另一个路径通过软链接切换。顺带提一句“解压文件乱码”这个场景从 Windows 那边拿来的压缩包在 Linux 下解压后文件名变成乱码这其实是文件名编码问题不是可执行文件本身的格式问题。文件内容可能完全正常只是名字乱码导致你cd进去都费劲。用unzip -O GBK指定编码后重新解压文件名正常了自然就能正常执行了。4.2 setuid、setgid 与 sticky bit特殊权限位的安全边界除了rwxLinux 还有三个特殊权限位setuid、setgid 和 sticky bit。它们的数字前缀分别是 4、2、1比如chmod 4755、chmod 2755、chmod 1777也可以分别用chmod us、chmod gs、chmod t来设置。setuid 位的行为很有意思当一个可执行文件被设置了 setuid用户执行这个文件时进程的有效用户 ID 会变成文件所有者的 ID而不是当前登录用户的 ID。最经典的例子是/usr/bin/passwd$ ls -l /usr/bin/passwd -rwsr-xr-x 1 root root ...注意第一组权限里的rws这个s就是 setuid。普通用户执行passwd修改密码时需要写/etc/shadow这个只有 root 才能写的文件。就是因为 passwd 有 setuid 位进程临时以 root 身份运行才允许普通用户完成这个操作。但这也是安全风险最集中的地方。网络上聊到“linux 提权”很大一部分利用路径就是找系统里错误配置的 setuid 文件如果一个属于 root 的可执行文件被设置了 setuid而它本身又有漏洞普通用户执行它就可能获得 root 权限。所以防御思路上有几件事值得养成习惯不要随便给自写程序加setuid位除非你非常清楚它的安全边界定期检查系统里有哪些 setuid 文件find / -perm -4000 -type f 2/dev/null普通用户尽量少用sudo去运行来历不明的可执行文件。setgid 位在目录上还有个特殊用途给共享目录设置 setgid 后新建文件会自动继承目录的组而不是创建者的默认组多人在同一目录协作时非常有用。/tmp目录则用了 sticky bitdrwxrwxrwt最后一个t它的作用是虽然目录对所有人可写但只有文件所有者和 root 能删除目录里的文件避免“我删你临时文件”的混乱。另外还有一个容易忽略的细节脚本文件上的 setuid 位通常被内核和解释器直接忽略。也就是说你给一个#!/bin/bash脚本加chmod us期望它像/usr/bin/passwd那样以 root 身份运行多半不会生效因为 bash 在启动脚本时会主动放弃 setuid 权限。这也是系统出于安全考虑做出的取舍。这套内容看着基础但确实是我踩了不少坑之后才真正想通的。刚入门的时候总觉得 Linux 命令很神秘文件跑不起来就急着去网上找答案。其实核心就三件事权限位决定你能不能执行格式和解释器决定怎么执行PATH 决定你在哪找它执行。把这三件事理清楚后面再学进程管理、shell 编程都会顺畅很多。最后再分享一个小习惯遇到任何一个“跑不起来”的可执行文件先跑一遍file、ldd、ls -l三件套大概率能定位九成的问题。
返回列表