首页 > 解决方案 > 为什么内存(x86 / nasm)中的数据段之间有空的地址空间?

问题描述

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

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')

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

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

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

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

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

NASM 版本 2.11.08 架构 x86 | Ubuntu 16.04

标签: assemblyx86nasmx86-64

解决方案


数据对齐

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

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 默认值。请参阅手册

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

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

3) 空间大小是如何计算的?

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

内存访问的微妙之处很多。如需更深入的解释,请参阅wiki和大量在线文章,例如此处;对于你们当中的受虐狂,总是有手册!

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

……………………………………………………………………………………………………………………………… ……………………………………………………………………………………………………………………

解决原来的问题:

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

因此,最好在制作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

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


推荐阅读