【问题标题】:Why are there empty address spaces between data sections in memory (x86 / nasm)?为什么内存(x86 / nasm)中的数据段之间有空的地址空间?
【发布时间】:2018-12-27 13:11:52
【问题描述】:

我正在尝试编写一个小程序,询问用户他们的姓名,对用户输入进行编码,然后将一条消息打印到标准输出,详细说明编码的输入。例如,用户输入姓名‘John’,它会在标准输出中打印“Your code name is: Red5”。

SECTION .data              ; Section containing initialised data

    RequestName: db "Please enter your name: "
    REQUESTLEN: equ $-RequestName

    OutputMsg: db "Your code name is: "
    OUTPUTLEN: equ $-OutputMsg

SECTION .bss               ; Section containing uninitialized data  

    EncodedName: resb ENCODELEN
    ENCODELEN: equ 1024

我的输出消息的第一部分,“你的代号是:”,存储(开始)在内存地址“OutputMsg”,输出消息的第二部分,将是编码的用户输入“Red5 ”,存储在内存地址“EncodedName”。因此,要将所需的消息打印到标准输出,我使用以下代码将两者连接起来:

mov rdx,OUTPUTLEN    ; Length of string 'OutputMsg'
add rdx,r8           ; r8 contains the number of bytes entered by the user
                     ; the code name is always equ in length to user input
mov rax,4            ; sys_write
mov rbx,1            ; stdout
mov rcx,OutputMsg    ; Offset of string to print to stdout
int 80h              ; Make kernel call

这几乎可以按预期工作。但是,输出中缺少最后一个字符。因此,我得到的不是“你的代号是:Red5”,而是“你的代号是:Red5”。在调试器中检查内存时,在“OutputMsg”的末尾和“EncodedName”的偏移量之间错误地“放置”了一个空内存地址 (0x00)。

Address         Binary    ASCII     
0x… 60012a      0x20      Space  (This is the end of the data item ‘OutputMsg’)
0x… 60012b      0x00      NUL
0x… 60012c      0x52      R (The start of SECTION .bss / 'EncodedName')

我已经使用其他几个代码示例对此进行了测试,在内存中SECTION .data 的结尾和SECTION .bss 的开头之间似乎总是存在NUL 字符的“随机”位置。

1) 是什么导致了这个空地址空间,因为它没有包含在我的源代码中?

2) 在我查看的所有示例中,空地址空间出现在 SECTION .data 的末尾,因此我认为这是预期的行为。这个空地址空间的具体原因是什么,是为了“标记”一个部分的结尾和下一个部分的开头吗?为什么有必要这样做?

3) 空间大小是如何计算的。我发现根据程序和我正在查看的部分,有时这个空间是一个字节,有时是两个/三个;我如何在运行前知道这个空白空间有多少字节?

我可以解决这个问题。但是,我想了解发生了什么。我编写了将两个SECTIONS 之间的字符串连接起来的代码,以便打印到标准输出。我无法解释的意外空地址空间正在影响我的计算。

NASM 版本 2.11.08 架构 x86 | Ubuntu 16.04

【问题讨论】:

  • 通常你不应该依赖于不同部分的相对位置。它们甚至可能不在给定的顺序中。它们之间的填充由对齐要求确定。此外,部分内容可能来自多个文件,然后由链接器合并。
  • 好吧,根据the manual NASM 默认假设大多数部分是 4 字节对齐。以0xb 结尾的地址不是4 字节对齐的,而以0xc 结尾的地址是。
  • 对于较大的.bss 或较大的.data,它们可能会位于不同的页面中,它们之间会有一些未映射的页面。由链接器决定如何将节映射到 ELF 可执行段 (What's the difference of section and segment in ELF file format),以及是否将它们标记为映射或复制到内存中。
  • 顺便说一句,What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?。您应该将代码移植到 syscall,除非您出于某种特定原因使用 32 位 int 0x80
  • 您可以使用 gcc -m32 构建 32 位二进制文​​件,而不是在学习的同时尝试将示例移植到 x86-64。 (例如,gcc -m32 -static -nostdlib foo.s 汇编 + 链接 32 位静态可执行文件)。顺便说一句,“x64”仅在 Windows 上使用。架构的实际名称是 x86-64。

标签: assembly x86 nasm x86-64


【解决方案1】:

数据对齐

通常将内存视为扁平的字节数组:

Data:       | 0x50 | 0x69 | 0x70 | 0x43 | 0x68 | 0x69 | 0x70 | 0x73 | 
Address:    |  0   |   1  |  2   |  3   |  4   |   5  |   6  |   7  |   
ASCII:      |  P   |   i  |  p   |  C   |  h   |   i  |   p  |   s  |

但是,CPU 本身不会一次读取或写入数据到内存中的一个字节。效率是游戏的名称,因此,计算机的 CPU 会从内存中读取数据,一次固定的字节数。处理器访问内存的大小称为其内存访问粒度 (MAG)。

内存访问粒度因架构而异。作为一般规则,MAG 等于所讨论的处理器 IE 的本机字长。 IA-32 的粒度为 4 字节。

如果 CPU 一次只能从内存中读取一个字节,则它需要访问内存 8 次才能读取整个上述数组。将此与 CPU 一次访问 4 个字节的内存进行比较,即 4 个字节的粒度。在这种情况下,CPU 只需要访问内存两次; 1 = 字节 0-3,2 = 字节 4-7。

内存对齐在哪里发挥作用:

好吧,让我们假设一个 4 字节的 MAG。正如我们所见,为了从内存中读取字符串“PipChips”,CPU 需要访问内存两次。现在,让我们假设数据在内存中的对齐方式略有不同。假设如下:

Data:       | 0x6B | 0x50 | 0x69 | 0x70 | 0x43 | 0x68 | 0x69 | 0x70 | 0x73 |  
Address:    |   0  |   1  |   2  |   3  |   4  |   5  |   6  |  7   |   8  |    
ASCII:      |   k  |   P  |   i  |   p  |   C  |   h  |   i  |  p   |   s  | 

在本例中,要访问相同的数据,CPU 总共需要访问内存 3 次; 1 = 字节 0-3,2 = 字节 4-7,第三次访问内存地址 8 处的“s”。此外,处理器必须执行额外的工作,以便移出不需要的字节,即由于数据存储在未对齐的地址,因此不必要地从内存中读取。

这就是内存对齐发挥作用的地方。 CPU有一个MAG,主要目的是提高机器效率。因此,对齐内存中的数据以匹配机器内存访问边界可以创建更高效​​的代码。

这是对内存对齐的(n)(过度)简单化解释,但它回答了问题:

1) 是什么导致了这个空地址空间,因为它没有包含在我的源代码中?

“空地址空间”是由SECTION 数据的对齐要求生成的。如果您没有为部分属性指定值,则假定为 NASM 默认值。请查看manual

2)这个空地址空间的具体原因是什么?

对齐内存数据的首要原因是软件效率和稳健性。如前所述,处理器将以字长为粒度访问内存。

3) 空间大小如何计算?

汇编器将填充该部分,以便紧随其后的数据自动对齐到指定内存访问边界的实例。在最初的问题中,section .data 将在地址 0x… 60012a 结束,在没有必要的填充的情况下,section .bss 从地址 60012b 开始。在这里,数据不会与 CPU 访问粒度定义的内存访问边界正确对齐。因此,NASM 在其智慧中添加了一个 nul 字符的填充,以便将内存地址向上舍入到下一个 可被 4 整除的地址,从而正确对齐数据.

内存访问的微妙之处很多;如需更深入的解释,请参阅wiki,以及众多在线文章,例如here;对于你们当中的自虐狂来说,手册总是有的!

通常,数据对齐由编译器/汇编器自动处理,尽管程序员控制是一种选择,并且在某些情况下是可取的。

………………………………………………………………………………………………………………....... .........................

解决原来的问题:

我们仍然存在如何连接我们的两个字符串以进行输出的问题。我们现在知道,至少可以说,跨节连接两个字符串的实现并不理想。通常,在运行期间,我们不会知道这些部分相对于彼此放置的位置。

因此,最好在创建syscall 之前将这些字符串连接到内存中的一个区域中;与依赖系统调用来提供连接相反,它基于字符串应该在内存中的位置的假设。

我们有几种选择:

  1. 连续调用两个sys_write,以便打印两个字符串,并在输出中给出它们是一个的错觉:虽然直截了当,但这没有什么意义,因为系统调用很昂贵。

  2. 直接将用户输入读取到位:至少乍一看,这似乎是合乎逻辑且最有效的做法。因为我们可以在不移动任何数据的情况下编写字符串,并且只有一个syscall。然而,我们面临着无意中覆盖数据的问题,因为我们没有保留内存中的空间。此外,将用户输入读取到初始化的.data 部分似乎是“错误的”;初始化数据是在程序开始之前有值的数据!

  3. 在内存中移动“EncodedName”,使其与“OutputMsg”连续:这看起来很简洁。然而,实际上它与选项 2 并没有什么不同,并且具有相同的缺点。

  4. 解决方案:sys_write系统调用之前,创建一个内存缓冲区并将字符串连接到这个内存缓冲区中。

    部分.bss

     EncodedName: resb ENCODELEN
     ENCODELEN: equ 1024
    
     CompleteOutput: resb COMPLETELEN
     COMPLETELEN: equ 2048  
    

用户输入将被读取到“EncodedName”。然后我们在“CompleteOutput”连接“OutputMsg”和“EncodedName”,准备写入标准输出:

    ; Read user input from stdin:
    mov rax,0                               ; sys_read
    mov rdi,0                               ; stdin
    mov rsi,EncodedName                     ; Memory offset in which to read input data
    mov rdx,ENCODELEN                       ; Length of memory buffer
    syscall                                 ; Kernel call
    
    mov r8,rax                              ; Save the number of bytes read by stdin
    
    ; Move string 'OutputMsg' to memory address 'CompleteOutput':
    mov rdi,CompleteOutput                  ; Destination memory address 
    mov rsi,OutputMsg                       ; Offset of 'string' to move to destination
    mov rcx,OUTPUTLEN                       ; Length of string being moved
    rep movsb                               ; Move string, iteration, per byte
    
    ; Concatenate 'OutputMsg' with 'EncodedName' in memory:
    mov rdi,CompleteOutput                  ; Destination memory address
    add rdi,OUTPUTLEN                       ; Add length of string already moved, so we append strings, as opposed to overwrite
    mov rsi,EncodedName                     ; Offset memory address of string being moved
    mov rcx,r8                              ; String length, during sys_read, the number of bytes read was saved in r8
    rep movsb                               ; Move string into place
    
    ; Write string to stdout:
    mov rdx,OUTPUTLEN                       ; Length of 'OutputMsg' 
    add rdx,r8                              ; add length of 'EncodedName' 
    
    mov rax,1                               ; sys_write
    mov rdi,1                               ; stdout
    mov rsi,CompleteOutput                  ; Memory offset of string
    syscall                                 ; Make system call

*归功于原始问题中的 cmets,为我指明了正确的方向。

【讨论】:

  • 顺便说一句,现代 CPU 可以加载/存储单个字节,而无需具有来读取/修改/写入包含的单词。 Can modern x86 hardware not store a single byte to memory?.
  • 回复:您可能的解决方案:您可以避免复制用户输入。 首先将OutputMsg.rodata 复制到.bss 中的大缓冲区,然后将sys_read 复制到紧随其后的位置。 (或 sys_read 先到位;您知道OutputMsg 的长度,因此您可以留出空间来复制它而无需实际复制它。首先访问内核内部的页面可能会使页面错误处理更有效。)你是对的,在.data 中放入足够多的显式零是不优雅的,因为它们实际上会存储在可执行文件中,而不仅仅是保留空间。
  • 要将RDX设置为正确的长度,您可以使用lea edx, [rax + OUTPUTLEN]rep movsb 不会破坏 RAX,或者您可以在复制之前这样做。您不需要 64 位操作数大小;请记住,您的缓冲区只有 2k,sys_read 不能返回不适合 32 位的大小。您可以直接使用CompleteOutput + OUTPUTLEN,而不是在运行时使用add。例如mov rsi, CompleteOutput + OUTPUTLENsyscall 之前读入到位。 (或者更好,mov esi, ...lea rsi, [rel CompleteOutput + OUTPUTLEN]。64 位立即数 mov 是地址的糟糕选择。)
  • SO 不接受任意 HTML(并对您可以链接到的站点设置限制,例如,没有 URL 缩短器);它可能吃了部分标签。
  • 任何链接时间常量都可以在构建时完成。它不应该那么混乱。寻址模式不会改变,但汇编器会创建一个目标文件,要求链接器为符号名称添加偏移量。如果你写了int foo(){ return static_array[10]; },编译器会做同样的事情,例如mov eax, [static_array + 40] 该地址本身没有标签,但它与标签的距离已知,因此您可以相对于该标签进行引用。 (或者就像我在上一条评论中所说的那样,putresb msglen / label: resb some_more 标记你想要的位置
猜你喜欢
  • 2013-01-22
  • 2019-07-03
  • 2019-02-19
  • 2011-10-06
  • 2011-06-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-08
相关资源
最近更新 更多