【发布时间】:2013-01-26 13:20:24
【问题描述】:
我正在做 Linux 汇编,我知道它有一个平面内存模型。我对 NEAR 和 FAR JMP 感到困惑。
NEAR 在同一个段,而 FAR 是另一个段。据我了解,linux虚拟内存中没有段?另外,我们如何知道我的程序代码是否按多个段布局?
【问题讨论】:
我正在做 Linux 汇编,我知道它有一个平面内存模型。我对 NEAR 和 FAR JMP 感到困惑。
NEAR 在同一个段,而 FAR 是另一个段。据我了解,linux虚拟内存中没有段?另外,我们如何知道我的程序代码是否按多个段布局?
【问题讨论】:
已经很久没有分段了。保护模式 x86 中的正确术语是 selector。
话虽如此,近跳转和远跳转的区别在于前者保持相同的代码选择器cs,而后者(通常)会更改它。
在平面内存模型中,前一种情况几乎总是如此。
您可以拥有一个操作系统,其中平面内存模型由多个选择器提供服务,但我看不到它的有用用例,而且这不是 Linux 的工作方式,至少在x86。
【讨论】:
NEAR 在同一个段,而 FAR 是另一个段。
近跳转跳转到当前代码段中的一个位置(由cs 指向)。远跳转通常用于跳转到不同代码段内的位置,但如果远地址中的段选择器与cs 中的值一致,它也可以跳转到当前段内的位置。
据我了解,linux 虚拟内存中没有段?
如果发现 Linux 端口使用某种分段内存连接到 CPU,我不会感到惊讶。所以,我会说这取决于。不过,您不太可能在 x86 平台上看到 Linux 使用段。但同样,您或其他人可以制作一个在实模式下运行并使用分段的小型 Linux。
另外,我们如何知道我的程序代码是否分多个段布局?
您检查 CPU 和操作系统。当然,如果您编写可移植的 C 代码,这应该与您无关。
【讨论】:
据我了解,linux 虚拟内存中没有段?
足够准确。有特定于线程的 data,其位置由 %fs 段 base 指向,但没有适合远跳转的段。
另外,我们如何知道我的程序代码是否以多种方式布局 细分?
如果您的目标平台是 Linux,那么您已经知道它不是。 (如果任何现代操作系统仍然以使jump far 有意义的方式使用段,我会感到惊讶)。
【讨论】:
int、sysenter 或syscall 进入内核模式,iret 从内核堆栈恢复cs:eip 或cs:rip,或sysret。除非您想在以 64 位启动的进程中执行不稳定的愚蠢计算机技巧,例如更改为 32 位模式,否则jmp far 的理由为零。
现代主流操作系统(如 Linux)中使用的平面内存模型使得分段大多已过时,而且(幸运的是)您无需担心。
在页表支持 NX 位将页面标记为不可执行之前,有一些工作是使用段限制来避免执行可写内存(尤其是堆栈),这使得缓冲区溢出攻击比仅仅返回到外壳代码。例如Exec Shield (lwn article) 从 2003 年开始。
我忘记了这实际上是如何工作的,我认为这主要是在排除堆栈的 CS 上设置了段限制,而不是为每个代码块(主可执行文件 + 每个动态库)使用带有新段描述符的 far jmp。
但幸运的是,现代 x86 可以使用具有 NX 位(PAE 或 x86-64)的现代页表,这意味着用户空间可以像读取和写入权限一样设置正常的每页执行权限(@987654324 @、mprotect 和程序初始部分的 ELF 元数据,如堆栈、r/w 数据和文本 + 只读数据)。或者对于非 Linux,当然是它们等效的系统调用和元数据。
但如果操作系统是 Linux 并且已经在保护模式 + 平面内存模式下运行,那么我们还需要 Far JMP 吗?
不,在 Linux 的用户空间或内核模式下,您永远不需要 far jmp, 而且创建一个是个坏主意。
您可能很想使用远 jmp ptr16:32 编码直接跳转到绝对地址(新的 CS 值被硬编码为 Linux 已知用于 32 位用户的相同 CS 值-空间)。但这比jmp rel32 附近的普通地址要慢得多,后者可以从任何其他 32 位地址到达任何 32 位地址。 (直接近跳转仅适用于相对位移,而不是绝对目标。如果您不知道自己的自己的地址来计算相对位移,则需要间接跳转以近跳转到绝对地址。 )
这在 64 位模式下甚至都不是一个选项,其中没有 jmp far 80-bit immediate ptr16:64 编码,只有内存间接。因此,如果跳转目标对于rel32 编码来说太远,您可以像普通人一样使用mov rax, imm64 / jmp rax。
Linux 上的所有用户空间进程使用相同的 32 位或 64 位 CS 段选择器(当前特权级别 CPL = 3 = 环 3 用户模式),内核使用不同的(CPL=0 = ring 0 内核模式)。
现代 x86 操作系统上 CS 的唯一目的是选择 32 位和 64 位模式(GDT 条目中的 .L 位)和权限级别。
您只能通过中断/异常和指令在用户和内核 CS 之间切换,例如 int、sysenter 或 syscall 进入内核模式,iret 从内核恢复 cs:eip 或 cs:rip堆栈,或sysexit(32 位内核)或sysret,用于优化从系统调用返回到用户空间。在首先进入保护模式(带有jmp far)后,内核不会jmp far 更改CS。
除非您想在以 64 位启动的进程中执行不稳定的愚蠢计算机技巧,例如更改为 32 位模式,否则在 Linux 下jmp far 是零理由。
这是可能的,但我不知道它是否真的稳定。例如内核可能记得您的进程应该是 64 位的,并从 64 位模式的中断返回。 (即异步将 CS 设置为硬编码的USER32_CS 常量,而不是恢复旧值。)IIRC,它在使用sysret 的syscall 返回路径中执行此操作,请参阅What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?
你想这样做吗?你不可以。除了具有BITS 32 与BITS 64 指令的汇编程序之外,任何工具链都对这样做的支持为零,基本上为零收益,并且崩溃的风险很大(您的进程,而不是机器)。在 32 位模式下您可以在手写 asm 中执行的任何操作,在 64 位模式下使用分配有 mmap(MAP_32BIT) 的 32 位指针或使用 x32 ABI 也可以执行。
我猜可能在原始 Core 2 上(其中 cmp/jcc 宏融合仅在 32 位模式下工作),在 32 位模式下运行循环并且仅使用 64 位模式可能会有性能优势会占用大量内存,但切换基本上会消耗管道刷新,因此通常只展开一点会更便宜,而不是切换到 32 位模式并返回 64 以进行特定的长时间运行循环。
【讨论】:
FAR 和 NEAR 控制传输指令基本上是一种控制传输协议 通常,我们看到程序按顺序从上到下逐行执行,有时需要将控制从一个位置转移到另一个位置 NEAR - 如果您想将控制权转移到当前代码段中的内存位置,那么它被称为 NEAR(内部段) 如果控制转移到当前代码段之外,则称为 FAR 跳转 在 FAR 中,因为控件在当前代码段之外传递,CS(代码段)和 IP(指令指针)都必须更新为新值
【讨论】: