为什么内存中的数据段之间没有空的地址空间(x86 / nasm)?

时间:2018-07-19 11:49:11

标签: assembly x86 nasm x86-64

我正在尝试编写一个小程序,询问用户名称,对用户输入进行编码,然后将一条消息打印到stdout,详细说明编码后的输入。例如,用户输入名称“ John”,它将在标准输出上显示“您的代号为: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”中。因此,为了将所需的消息打印到stdout,我使用以下代码将两者串联:

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 .data结束与{{1 }}开始。

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

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

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

我可以解决这个问题。但是,我想了解发生了什么。我编写了将两个SECTION .data之间的字符串连接起来的代码,以便打印到stdout。我无法解释的意外的空地址空间正在使我的计算中断。

NASM 2.11.08版x86体系结构| Ubuntu 16.04

1 个答案:

答案 0 :(得分:0)

数据对齐

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

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,其主要目的是提高机器效率。因此,对齐内存中的数据以匹配机器的内存访问边界将创建更有效的代码。

这是对内存对齐的一种(过于简单的解释),但是它回答了这个问题:

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系统调用之前,创建一个内存缓冲区并将字符串连接到该内存缓冲区中。

SECTION .bss

    EncodedName: resb ENCODELEN
    ENCODELEN: equ 1024

    CompleteOutput: resb COMPLETELEN
    COMPLETELEN: equ 2048  

用户输入将被读取为“ EncodedName”。然后,我们在“ CompleteOutput”处将“ OutputMsg”和“ EncodedName”串联起来,准备写入stdout:

    ; 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

*由于原始问题中的注释而致谢,用于向正确的方向指出我。