【问题标题】:syscall read/write 1 byte only?系统调用仅读/写 1 个字节?
【发布时间】:2020-08-03 19:39:52
【问题描述】:

我是汇编新手,正在尝试编写一个“echo”内置版本,但一次只能操作 1 个字节。

我有以下按我想要的方式工作,除了它在读取和写入时都溢出超过 1 个字节,即使我在两个系统调用中明确说 x2 中的 1 个字节。我做错了什么?

示例运行:

sh-4.2$ ./echo1b
f
f
o
o
b
b
bar
bar
bazbazbaz
bazbazbaz
q
sh-4.2$

代码如下:

.data
temp:   .byte 1  

.text
.globl _start
_start:
    /* read one byte from stdin, store to temp */
    mov x0, #0x0
    adr x1, temp
    mov x2, #0x1
    mov x8, #0x3F
    svc #0x0

    /* write newline to stdout */
    mov x0, #0x1
    mov x1, #0xA
    mov x2, #0x1
    mov x8, #0x40
    svc #0x0
    
    /* if byte in temp is "q", exit */
    mov x5, #0x71
    ldr x1, temp
    cmp x1, x5
    beq exit

    /* otherwise, write it to stdout and repeat */
    mov x0, #0x1
    adr x1, temp
    mov x2, #0x1
    mov x8, #0x40
    svc #0x0
    b _start

exit:
    /* exit cleanly */  
    eor x0, x0, x0 
    eor x1, x1, x1
    eor x2, x2, x2
    mov x8, #0x5D
    svc #0x0

【问题讨论】:

  • 你写错了换行符。 write 需要一个地址而不是一个值。您需要执行类似newline: .byte 10 的操作,然后传递其地址。
  • 顺便说一下,这更像cat 而不是echo
  • @Jester 谢谢!即使删除整个换行部分,read 调用也会读取超过 1 个字节,而底部的 write 调用会将其写出。我怎样才能将这些调用限制在 1 个字节(或 x 个字节),因为 x2 中的“计数”字段并没有真正做任何事情。
  • 不,它一次只读取一个字节并打印它。然后它循环并读取并打印另一个。如果你修复换行部分而不是删除它,你会看到。
  • @old_timer ubuntu server 20.04 LTS 64-bit 在树莓派 4 上运行

标签: linux assembly buffer-overflow arm64


【解决方案1】:

您的代码中有几个问题:

  • 如 cmets 中所述,在调用 sys_write 时,输出缓冲区的地址必须位于 x1 中,就像调用 temp 一样
  • temp 与换行符进行比较时,您必须使用ldrb w1, [x0] 而不是ldr x1, temp,其中x0 指向temp。后者将读取 4 个字节,但不能保证前三个字节为零。

我还改进了您的代码的某些部分:

  • cmp 可以与 12 位立即数一起使用,因此无需将 0x71 放入寄存器中。
  • 将第二个 sys_write 调用移到 _start 之前可避免无条件跳转。
  • sys_exit 仅使用 x0 作为参数,因此无需将 x1x2 设置为零。

这是在 Raspbian 4.19(基于 debian)上测试的最终代码:

.data
    temp:    .byte 1
    newline: .byte 0x0A

.text
.globl _start

loop:
    // 4: Otherwise, write it to stdout and repeat
    mov  x0, #0x1    // int    fd
    adr  x1, temp    // void*  buf
    mov  x2, #0x1    // size_t count
    mov  x8, #0x40   // sys_write
    svc  #0x0
    
_start:
    // 1: Read one byte from stdin and store to temp (including newline)
    mov  x0, #0x0   // int    fd
    adr  x1, temp   // void*  buf
    mov  x2, #0x1   // size_t count
    mov  x8, #0x3F  // sys_read
    svc  #0x0
    
    // 2: If byte in temp is 'q', exit
    adr  x0, temp
    ldrb w1, [x0] // instead of temp
    cmp  x1, #0x71
    bne  loop

    // 5: Exit cleanly
    eor  x0, x0, x0  // int status
    mov  x8, #0x5D   // sys_exit
    svc  #0x0

评论后编辑:要在退出时刷新标准输入,您可以在第 5 步之前添加以下行:

    // 5: Flush stdin (read until newline)
flush:
    mov  x0, #0x0   // int    fd
    adr  x1, temp   // void*  buf
    mov  x2, #0x1   // size_t count
    mov  x8, #0x3F  // sys_read
    svc  #0x0

    adr  x0, temp
    ldrb w1, [x0]
    cmp  x1, #0x0A
    bne flush       // loop until x0 == 0x0A

【讨论】:

  • 非常感谢,非常有帮助。如果您有时间,还有一个问题,如果“q”位于行缓冲区的开头或中间,则使用此代码和上面的代码,它将把整行刷新到标准输出(并假设它是一个命令。)。你会如何建议刷新缓冲区,以免它这样做?
  • 在程序集级别没有fflush 命令。正如stdin using fgets 中提到的,您可以阅读直到换行来实现这一点。我在我的答案中添加了一些代码来做到这一点。
  • @Sk6:如果你想读取标准输入到 EOF,你可以用更大的缓冲区更有效地做到这一点。理论上,如果输入是从一个大文件重定向的,则可能需要超过 1 个read 系统调用,但从终端来看,一次 4k 读取可能会得到所有内容。 (如果可用的数据较少,read 会以较小的计数返回。)我称之为“耗尽”标准输入;刷新对输出流更有意义。如果你不这样做会发生什么,后面的字节仍然处于未读状态,仍然在内核的该文件的缓冲区(tty)中,所以当你的程序退出时,shell 会读取它。
  • 当然,如果你想在退出时耗尽标准输入,没有必要做一次读取 1 个字节这样低效的事情。在你的输入循环中,你可以读入一个更大的缓冲区,然后循环读取的字节数,与\n 交错,然后做一个大的write。 (AArch64 具有 SIMD,因此您可以非常有效地进行交织,一次 8->16 个字节,使用 vzip.8 或其他东西。)
  • (该链接适用于 32 位 ARM。AArch64 对其 SIMD 指令的命名略有不同。)
猜你喜欢
  • 1970-01-01
  • 2012-09-13
  • 1970-01-01
  • 2011-10-24
  • 1970-01-01
  • 2015-04-16
  • 1970-01-01
  • 1970-01-01
  • 2021-07-06
相关资源
最近更新 更多