ELF和静态链接:为什么程序无法同时在Linux和Windows下运行?
目录
疑问
编译、链接和装载:拆解程序执行
ELF 格式和链接:理解链接过程
小结
疑问
既然我们的程序最终都被变成了一条条机器码去执行,那为什么同一个程序,在同一台计算机上,在 Linux 下可以运行,而在 Windows 下却不行呢?反过来,Windows 上的程序在 Linux 上也是一样不能执行的。可是我们的 CPU 并没有换掉,它应该可以识别同样的指令呀?
如果你和我有同样的疑问,那这一节,我们就一起来解开。
编译、链接和装载:拆解程序执行
我们知道,写好的 C 语言代码,可以通过编译器编译成汇编代码,然后汇编代码再通过汇编器变成 CPU 可以理解的机器码,于是 CPU 就可以执行这些机器码了。你现在对这个过程应该不陌生了,但是这个描述把过程大大简化了。下面,我们一起具体来看,C 语言程序是如何变成一个可执行程序的。
我们先把前面的 add 函数示例,拆分成两个文件 add_lib.c 和 link_example.c。
// add_lib.c
int add(int a, int b)
{return a+b;
}
// link_example.c#include <stdio.h>
int main()
{int a = 10;int b = 5;int c = add(a, b);printf("c = %d\n", c);
}
我们通过 gcc 来编译这两个文件,然后通过 objdump 命令看看它们的汇编代码。
$ gcc -g -c add_lib.c link_example.c
$ objdump -d -M intel -S add_lib.o
$ objdump -d -M intel -S link_example.o
add_lib.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add>:0: 55 push rbp1: 48 89 e5 mov rbp,rsp4: 89 7d fc mov DWORD PTR [rbp-0x4],edi7: 89 75 f8 mov DWORD PTR [rbp-0x8],esia: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]d: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]10: 01 d0 add eax,edx12: 5d pop rbp13: c3 ret
link_example.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:0: 55 push rbp1: 48 89 e5 mov rbp,rsp4: 48 83 ec 10 sub rsp,0x108: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xaf: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x516: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]19: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]1c: 89 d6 mov esi,edx1e: 89 c7 mov edi,eax20: b8 00 00 00 00 mov eax,0x025: e8 00 00 00 00 call 2a <main+0x2a>2a: 89 45 f4 mov DWORD PTR [rbp-0xc],eax2d: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]30: 89 c6 mov esi,eax32: 48 8d 3d 00 00 00 00 lea rdi,[rip+0x0] # 39 <main+0x39>39: b8 00 00 00 00 mov eax,0x03e: e8 00 00 00 00 call 43 <main+0x43>43: b8 00 00 00 00 mov eax,0x048: c9 leave 49: c3 ret
既然代码已经被我们“编译”成了指令,我们不妨尝试运行一下 ./link_example.o。
不幸的是,文件没有执行权限,我们遇到一个 Permission denied 错误。即使通过 chmod 命令赋予 link_example.o 文件可执行的权限,运行./link_example.o 仍然只会得到一条 cannot execute binary file: Exec format error 的错误。
我们再仔细看一下 objdump 出来的两个文件的代码,会发现两个程序的地址都是从 0 开始的。如果地址是一样的,程序如果需要通过 call 指令调用函数的话,它怎么知道应该跳转到哪一个文件里呢?
这么说吧,无论是这里的运行报错,还是 objdump 出来的汇编代码里面的重复地址,都是因为 add_lib.o 以及 link_example.o 并不是一个可执行文件(Executable Program),而是目标文件(Object File)。只有通过链接器(Linker)把多个目标文件以及调用的各种函数库链接起来,我们才能得到一个可执行文件。
我们通过 gcc 的 -o 参数,可以生成对应的可执行文件,对应执行之后,就可以得到这个简单的加法调用函数的结果。
$ gcc -o link-example add_lib.o link_example.o
$ ./link_example
c = 15
实际上,“C 语言代码 - 汇编代码 - 机器码” 这个过程,在我们的计算机上进行的时候是由两部分组成的。
第一个部分由编译(Compile)、汇编(Assemble)以及链接(Link)三个阶段组成。在这三个阶段完成之后,我们就生成了一个可执行文件。
第二部分,我们通过装载器(Loader)把可执行文件装载(Load)到内存中。CPU 从内存中读取指令和数据,来开始真正执行程序。
ELF 格式和链接:理解链接过程
程序最终是通过装载器变成指令和数据的,所以其实我们生成的可执行代码也并不仅仅是一条条的指令。我们还是通过 objdump 指令,把可执行文件的内容拿出来看看。
link_example: file format elf64-x86-64
Disassembly of section .init:
...
Disassembly of section .plt:
...
Disassembly of section .plt.got:
...
Disassembly of section .text:
...6b0: 55 push rbp6b1: 48 89 e5 mov rbp,rsp6b4: 89 7d fc mov DWORD PTR [rbp-0x4],edi6b7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi6ba: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]6bd: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]6c0: 01 d0 add eax,edx6c2: 5d pop rbp6c3: c3 ret
00000000000006c4 <main>:6c4: 55 push rbp6c5: 48 89 e5 mov rbp,rsp6c8: 48 83 ec 10 sub rsp,0x106cc: c7 45 fc 0a 00 00 00 mov DWORD PTR [rbp-0x4],0xa6d3: c7 45 f8 05 00 00 00 mov DWORD PTR [rbp-0x8],0x56da: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8]6dd: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]6e0: 89 d6 mov esi,edx6e2: 89 c7 mov edi,eax6e4: b8 00 00 00 00 mov eax,0x06e9: e8 c2 ff ff ff call 6b0 <add>6ee: 89 45 f4 mov DWORD PTR [rbp-0xc],eax6f1: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc]6f4: 89 c6 mov esi,eax6f6: 48 8d 3d 97 00 00 00 lea rdi,[rip+0x97] # 794 <_IO_stdin_used+0x4>6fd: b8 00 00 00 00 mov eax,0x0702: e8 59 fe ff ff call 560 <printf@plt>707: b8 00 00 00 00 mov eax,0x070c: c9 leave 70d: c3 ret 70e: 66 90 xchg ax,ax
...
Disassembly of section .fini:
...
你会发现,可执行代码 dump 出来内容,和之前的目标代码长得差不多,但是长了很多。因为在 Linux 下,可执行文件和目标文件所使用的都是一种叫ELF(Execuatable and Linkable File Format)的文件格式,中文名字叫可执行与可链接文件格式,这里面不仅存放了编译成的汇编指令,还保留了很多别的数据。
比如我们过去所有 objdump 出来的代码里,你都可以看到对应的函数名称,像 add、main 等等,乃至你自己定义的全局可以访问的变量名称,都存放在这个 ELF 格式文件里。这些名字和它们对应的地址,在 ELF 文件里面,存储在一个叫作符号表(Symbols Table)的位置里。符号表相当于一个地址簿,把名字和地址关联了起来。
我们先只关注和我们的 add 以及 main 函数相关的部分。你会发现,这里面,main 函数里调用 add 的跳转地址,不再是下一条指令的地址了,而是 add 函数的入口地址了,这就是 EFL 格式和链接器的功劳。
ELF 文件格式把各种信息,分成一个一个的 Section 保存起来。ELF 有一个基本的文件头(File Header),用来表示这个文件的基本属性,比如是否是可执行文件,对应的 CPU、操作系统等等。除了这些基本属性之外,大部分程序还有这么一些 Section:
-
首先是.text Section,也叫作代码段或者指令段(Code Section),用来保存程序的代码和指令;
-
接着是.data Section,也叫作数据段(Data Section),用来保存程序里面设置好的初始化数据信息;
-
然后就是.rel.text Secion,叫作重定位表(Relocation Table)。重定位表里,保留的是当前的文件里面,哪些跳转地址其实是我们不知道的。比如上面的 link_example.o 里面,我们在 main 函数里面调用了 add 和 printf 这两个函数,但是在链接发生之前,我们并不知道该跳转到哪里,这些信息就会存储在重定位表里;
-
最后是.symtab Section,叫作符号表(Symbol Table)。符号表保留了我们所说的当前文件里面定义的函数名称和对应地址的地址簿。
链接器会扫描所有输入的目标文件,然后把所有符号表里的信息收集起来,构成一个全局的符号表。然后再根据重定位表,把所有不确定要跳转地址的代码,根据符号表里面存储的地址,进行一次修正。最后,把所有的目标文件的对应段进行一次合并,变成了最终的可执行代码。这也是为什么,可执行文件里面的函数调用的地址都是正确的。
在链接器把程序变成可执行文件之后,要装载器去执行程序就容易多了。装载器不再需要考虑地址跳转的问题,只需要解析 ELF 文件,把对应的指令和数据,加载到内存里面供 CPU 执行就可以了。
小结
为什么同样一个程序,在 Linux 下可以执行而在 Windows 下不能执行了。其中一个非常重要的原因就是,两个操作系统下可执行文件的格式不一样。
我们今天讲的是 Linux 下的 ELF 文件格式,而 Windows 的可执行文件格式是一种叫作PE(Portable Executable Format)的文件格式。Linux 下的装载器只能解析 ELF 格式而不能解析 PE 格式。
如果我们有一个可以能够解析 PE 格式的装载器,我们就有可能在 Linux 下运行 Windows 程序了。这样的程序真的存在吗?没错,Linux 下著名的开源项目 Wine,就是通过兼容 PE 格式的装载器,使得我们能直接在 Linux 下运行 Windows 程序的。而现在微软的 Windows 里面也提供了 WSL,也就是 Windows Subsystem for Linux,可以解析和加载 ELF 格式的文件。
我们去写可以用的程序,也不仅仅是把所有代码放在一个文件里来编译执行,而是可以拆分成不同的函数库,最后通过一个静态链接的机制,使得不同的文件之间既有分工,又能通过静态链接来“合作”,变成一个可执行的程序。
对于 ELF 格式的文件,为了能够实现这样一个静态链接的机制,里面不只是简单罗列了程序所需要执行的指令,还会包括链接所需要的重定位表和符号表。
相关文章:

ELF和静态链接:为什么程序无法同时在Linux和Windows下运行?
目录 疑问 编译、链接和装载:拆解程序执行 ELF 格式和链接:理解链接过程 小结 疑问 既然我们的程序最终都被变成了一条条机器码去执行,那为什么同一个程序,在同一台计算机上,在 Linux 下可以运行,而在…...

【爬虫实战】python微博热搜榜Top50
一.最终效果 二.项目代码 2.1 新建项目 本文使用scrapy分布式、多线程爬虫框架编写的高性能爬虫,因此新建、运行scrapy项目3步骤: 1.新建项目: scrapy startproject weibo_hot 2.新建 spider: scrapy genspider hot_search "weibo.com" 3…...

【网络基础】——传输层
目录 前言 传输层 端口号 端口号范围划分 知名端口号 进程与端口号的关系 netstat UDP协议 UDP协议位置 UDP协议格式 UDP协议特点 面向数据报 UDP缓冲区 UDP的使用注意事项 基于UDP的应用层协议 TCP协议 TCP简介 TCP协议格式 确认应答机制&#…...

删除字符串特定的字符(fF)C语言
代码: #include <stdio.h> void funDel(char *str) {int i, j;for (i j 0; str[i] ! \0; i)if (str[i] ! f && str[i] ! F)str[j] str[i];str[j] \0; }int main() {char str[100];printf("请输入一个字符串:");gets(str);pr…...

C++入门(1):命名空间,IO流 输入输出,缺省参数
一、命名空间 1.1 命名空间的作用: 避免标识符命名冲突 1.2 命名空间定义: 关键字:namespace namespace test {// 命名空间内可以定义变量/函数/类型int a 10;int Add(int x, int y){return x y;}struct Stack{int* a;int top;int …...

Go 语言面试题(三):并发编程
文章目录 Q1 无缓冲的 channel 和 有缓冲的 channel 的区别?Q2 什么是协程泄露(Goroutine Leak)?Q3 Go 可以限制运行时操作系统线程的数量吗? Q1 无缓冲的 channel 和 有缓冲的 channel 的区别? 对于无缓冲的 channel,…...

Linux - make命令 和 makefile
make命令和 makefile 如果之前用过 vim 的话,应该会对 vim 又爱又恨吧,刚开始使用感觉非常的别扭,因为这种编写代码的方式,和在 windows 当中用图形化界面的方式编写代码的方式差别是不是很大。当你把vim 用熟悉的之后࿰…...

FPGA复习(功耗)
减小功耗 就得减小电流 电流和CF有关( C: 电容(被门数目和布线长度影响) F:时钟频率) 方法大纲 减小功耗:1 时钟控制 2输入控制 3减小供电电压 4双沿触发器 5修改终端 同步数字电路降低动态功耗:动态禁止…...

element ui el-table表格复选框,弹框关闭取消打勾选择
//弹框表格复选框清空 this.$nextTick(()>{this.$refs.table.clearSelection();})<el-table ref"table" v-loading"crud.loading" :header-cell-style"{ color: #FFF, background: #333 }":cell-style"{ color: #FFF, background: #3…...

数据结构——队列
1.队列元素逆置 【问题描述】 已知Q是一个非空队列,S是一个空栈。仅使用少量工作变量以及对队列和栈的基本操作,编写一个算法,将队列Q中的所有元素逆置。 【输入形式】 输入的第一行为队列元素个数,第二行为队列从首至尾的元素…...

【Unity引擎核心-Object,序列化,资产管理,内存管理】
文章目录 整体介绍Native & Managed Objects什么是序列化序列化用来做什么Editor和运行时序列化的区别脚本序列化针对序列化的使用建议 Unity资产管理导入Asset Process为何要做引擎资源文件导入Main-Assets和 Sub-Assets资产的导入管线Hook,AssetPostprocessor…...

Generics/泛型, ViewBuilder/视图构造器 的使用
1. Generics 泛型的定义及使用 1.1 创建使用泛型的实例 GenericsBootcamp.swift import SwiftUIstruct StringModel {let info: String?func removeInfo() -> StringModel{StringModel(info: nil)} }struct BoolModel {let info: Bool?func removeInfo() -> BoolModel…...

数据结构之手撕顺序表(增删查改等)
0.引言 在本章之后,就要求大家对于指针、结构体、动态开辟等相关的知识要熟练的掌握,如果有小伙伴对上面相关的知识还不是很清晰,要先弄明白再过来接着学习哦! 那进入正题,在讲解顺序表之前,我们先来介绍…...

进阶JAVA篇- ZoneId 类与 ZoneDateTime 类、Instant类的常用API(七)
目录 API 1.0 ZoneId 类的说明 1.1 如何创建 ZoneId 类的对象呢? 1.2 ZoneId 类中的 getAvailableZoneIds() 静态方法 2.0 ZoneDateTime 类的说明 2.1 如何创建 ZoneDateTime 类的对象呢? 3.0 Instant 类的说明 3.1 如何创建 Instant 类的对象呢…...

bat脚本字符串替换:路径中\需要替换,解决一些文件写入路径不对的问题
脚本 set dir_tmp%~dp0 set dir%dir_tmp:\\\\\% set dir_tmp%~dp0 新建一个变量dir_tmp,存储获取的脚本当前路径 set dir%dir_tmp:\\\\\% 新建一个变量dir ,存储字符串替换之后的路径 其中黄色的\\实际上代表的是一个\...

python一行命令搭建web服务,实现内网共享文件
python一行命令搭建web服务,实现内网共享文件 有时候我们在本地电脑访问自己的虚拟机的时候,可能因为某些原因无法直接CV文件到虚拟机。但此时我们又想上传文件到虚拟机,如果虚拟机和本地电脑可以互通。那么我们可以直接通过python来启动一个…...

RK3562开发板:升级摄像头ISP,突破视觉体验边界
RK3562开发板作为深圳触觉智能新推出的爆款产品,采用 Rockchip 新一代 64 位处理器 RK3562(Quad-core ARM Cortex-A53,主频最高 2.0GHz),最大支持 8GB 内存;内置独立的 NPU,可用于轻量级人工智能…...

数据结构与算法-队列
队列 🎈1.队列的定义🎈2.队列的抽象数据类型定义🎈3.顺序队列(循环队列)🔭3.1循环队列🔭3.1循环队列类定义🔭3.2创建空队列🔭3.3入队操作🔭3.4出队操作&#…...

腾讯云轻量2核4G5M可容纳多少人访问?
腾讯云2核4G5M服务器支持多少人在线访问?卡不卡?腾讯云轻量2核4G5M带宽服务器支持多少人在线访问?5M带宽下载速度峰值可达640KB/秒,阿腾云以搭建网站为例,假设优化后平均大小为60KB,则5M带宽可支撑10个用户…...

【分布式计算】九、容错性 Fault Tolerance
分布式系统应当有一定的容错性,发生故障时仍能运行 一些概念: 可用性Availability:系统是否准备好立即使用 可靠性Reliability:系统连续运行不发生故障 安全性:衡量安全故障的指标,没有严重事件发生 可维护…...

The SDK location is inside Studio install location 解决
The SDK location is inside Studio install location 解决 安装 Android Studio SDK 时提示:The SDK location is inside Studio install location 解决 问题: 由于 SDK 与 编辑器(Android Studio)的安装在同一目录下所以报错。 解决 你需要在 Andro…...

【蓝桥】数树数
一、题目 1、题目描述 给定一个层数为 n n n 的满二叉树,每个点编号规则如下: 具体来说,二叉树从上往下数第 p p p 层,从左往右编号分别为:1,2,3,4,…, 2p-1。 给你一条从根节点开始的路径࿰…...

2、Windows下安装
目录 一.安装 1、双击下载的程序: 2、加载完成后,会进入如下界面(选第一个Developer Default) 3、然后点击Next 点击Execute 然后Next 4.继续next注意端口为3306 5.继续next,输入账户密码(要有大小写…...

vue中transition的使用
Vue中的<transition>组件用于在元素或组件添加/移除时应用过渡动画。它能够包裹需要进行过渡效果的元素或组件,通过设置相应的CSS样式来实现过渡动画效果。 <transition name"过渡效果名称" before-enter"beforeEnter" enter"…...

性能测试中如何使用RunnerGo还原混合并发场景
我们在进行软件开发时经常需要进行性能测试、压力测试和负载测试。其中有一类测试场景叫做混合并发测试,需要模拟多个接口下不同数量的用户使用场景,检查同时处理多个并发任务的能力,本文将展示如何使用开源的RunnerGo还原混合并发场景。 在…...

KanziStudio described using object-oriented design patterns(持续更新...)
1.绑定-mvc mvc,model数据与view控件分离。...

线程同步的几种方式
目录 互斥锁条件变量读写锁信号量CAS-- 参考 线程同步方式有互斥锁,条件变量,信号量,读写锁,CAS锁等方式 互斥锁 互斥量 pthread_mutex_t在执行操作之前加锁,操作完之后解锁. 使用互斥量,来确保同一时刻只…...

Linux网络编程系列之服务器编程——多路复用模型
一、什么是多路复用模型 服务器的多路复用模型指的是利用操作系统提供的多路复用机制,同时处理多个客户端连接请求的能力。在服务器端,常见的多路复用技术包括select、poll和epoll等。这些技术允许服务器同时监听多个客户端连接请求,当有请求…...

在SQL语句里使用正则表达式,因该怎么使用
在SQL中使用正则表达式通常需要使用特定的函数或运算符,具体的语法可能因不同的数据库系统而有所不同。以下是使用正则表达式的一般方法,但请注意,具体语法可能会因您使用的数据库而有所不同。 一般情况下,您可以使用以下方法在S…...

扫码登录-测试用例设计
扫码登录测试用例...