我正在尝试编写一个小程序,询问用户名称,对用户输入进行编码,然后将一条消息打印到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
答案 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
*由于原始问题中的注释而致谢,用于向正确的方向指出我。