扫描一个字符串并在 64 位汇编气体中打印 strlen

scanf a string and print strlen in assembly gas 64-bit

我正在尝试使用 64 位 GAS 在汇编中编写一个 strlen 函数。 我需要从用户那里得到一个输入字符串,然后打印 它的长度。这是我的代码:

.lcomm d2, 255
.data
pstring1:  .ascii "%s[=10=]\n"

.text
.globl main
main:
    movq %rsp, %rbp 

    subq , %rsp   
    movq  $d2, %rsi
    movq  %rsi,%rbx          
    movq  $pstring1, %rdi
    movq  [=10=],%rax
    call scanf

    movq   , %rax
    movq   $d2, %rsi
    movq   $pstring1, %rdi
    call  printf #print to check if scanf worked write

    add   , %rsp

    movq 8(%rsp), %rcx
    movq %rcx, d2
    call pstrlen
    popq %rbx   
    ret

    ##########
pstrlen:  

    movq %rsp, %rbx
    movq 16(%rbp),%rdx
    xor %rax, %rax        
    jmp if

then:
    incq %rax
    movq $length,%rax
if:
    movq %rdx, %rcx
    cmp 0, %rcx
    jne then
end:
    pop %rbp
    ret

如果有人能举例说明如何在 64 位 GAS 程序集中使用字符串并将参数传递给函数,那将是理想的选择,因为我在网上找不到合适的东西。

原则上,您正在使用 .lcomm d2, 255 为字符串数据分配 255 个字节。一个字节是 8 位,1 位不是 0 就是 1。所以当作为无符号二进制值处理时,一个字节的最大值是 28-1。这对我来说是最常见的方式,我如何看待字节(作为数字 0..255),但是这 8 位也可以表示其他值,例如有时使用带符号的 8 位(-128..+127),或特定位被寻址,为访问它们的特定代码赋予它们特定的功能。 (这部分不错)

然后你使用 scanf"%s[=15=]\n" 定义(它将编译为字节 '%', 's', 0, 10 ...不确定空终止符之后的 10 有什么用处)。我会改用 .asciiz "%254s",以防止恶意用户向保留的 d2 space 输入超过 255 个字节的输入。 (注意它是 .asciiz,最后是 z,所以它会自己添加零字节)

那你就用printf。而是单独为输出提供另一个格式化字符串,这次像 formatOut: .asciiz "%s\n".

终于要strlen.

这意味着我将return返回输入。如果你是运行 in normal 64b OS (linux),你的输入字符串很可能是UTF-8编码的(除非你的OS设置在其他特定的Locale中,那么我不确定 scanf 选择哪个语言环境。

UTF-8编码是变长编码,所以你要决定你的strlen是return字符数,还是占用字节数。

为简单起见,我假设字节数(不是字符数)对您来说已经足够了,如果您的输入字符串仅包含基本的 7b ASCII 字符([0-9A-Za-z !@#$%^&*,.;'\<>?:"|{}] 等...请检查任何 ASCII table ...不允许重音字符(如 á),这将产生多字节 UTF8 代码),然后字节数也将等于字符数(UTF-8 编码有点像与 7b ASCII 兼容)。

这意味着例如对于输入 "Hell 1234",地址 d2 处的内存将包含这些值(十六进制)48 65 6C 6C 20 31 32 33 34 00。再一次,如果你检查 ASCII table,你会意识到例如字节 0x20 是 space 字符,等等......而字符串是 "nul terminated",最后一个值零是字符串的一部分,但不显示,而是被各种 C 函数用作 "end of string marker".

所以你想在 strlen 中做的是用 d2 地址加载一些寄存器,比方说 rdi。然后逐字节扫描(字节,因为ASCII编码是“1个字符=1个字节”的方式,我们将忽略UTF-8变长代码),直到内存中的值为零,同时统计有多少字节它确实需要达到它。如果你稍微思考一下这个想法,把它变成 "short" 换成 CPU,然后你会用 SCASB 来扫描(你也可以把它写成 "manually" 用普通的 mov/cmp/inc/jne/jnz 如果你愿意的话),你可以这样结束:

rdi = d2 address
rdx = rdi  ; (copy of d2 address)
ecx = 255  ; maximum length of string
al  = 0    ; value to test against
repne scasb  ; repeat SCASB instruction until zero is found
; here rdi points at the zero byte
; (or it's d2+255 if the zero terminator is missing)
rdi -= rdx ; rdi = length of string
; return result as you wish

所以你首先需要正确理解你正在操作的值是什么,它们在哪里,它们的 bit/byte 大小是多少,它有什么结构。

然后您可以编写指令,根据这些数据进行任何合理的计算。

在你的情况下,计算是 "length_of_string = number of non-zero bytes in 7b ASCII encoded string stored in memory at address d2"(我的意思是在成功 scanf 部分代码之后)。

考虑到你的源代码在我看来你不明白 x86 CPU 指令的作用,你只是从一些例子中复制它们。那会让你很快陷入困境。

例如 cmp 0, %rcx 正在检查 rcx(8 字节 "wide" 值)是否等于零。你确实用 rdx 中的值加载了 rcx,这是来自堆栈的东西(可能是 d2 地址),所以 rcx 永远不会为零。

即使你真的将内存中的字符值加载到 rcx,你也会同时加载其中的 8 个,所以你会错过 0 值在一些垃圾中只有一个字节,比如 0xCCCCCCCC00343332(我在 d2 缓冲区之后使用 0xCC 作为未定义的内存,例如,可能有任何值)。

因此该代码没有任何意义。如果您至少部分了解什么是 CPU 寄存器以及 mov/inc/cmp/... 等指令的作用,那么您就有机会通过简单地大量使用调试器来生成工作代码,以验证几乎每 1-2 条新指令添加到源代码,如果它确实操作了正确的值,并修复它们直到你做对了。

首先需要你清楚"correct behaviour"是什么! (就像在这种情况下 "fetching byte-by-byte values from d2 address, one after another, incrementing "length" 计数器,并寻找零字节)所以你可以判断代码何时执行你需要的操作。


我想通过这个答案指出的是,说明本身虽然重要,但不如您对 data/structures/algorithm 的看法重要。您的问题听起来好像您不知道 x86 程序集中的 "C string" 是什么,或者要使用哪种算法。这使您不可能只 "guess" 一些指令到源代码中,然后验证您是否猜对了。因为你不知道你想让它做什么。这就是为什么我告诉你还应该检查非 gas x86 Assembly 资源的最基本知识,什么是 bit/byte/computer memory/etc... 直到你稍微了解了哪些数值被操纵,例如创建"strings".

一旦你清楚它应该做什么,你就可以很容易地在调试器中捕捉到诸如交换参数之类的东西(例如:movq %rcx, d2 - 为什么你从 [=40 中放置 8 个字节=] into memory at address d2? That will overwrite the input string), and similar, 所以你实际上不需要100%很好地理解指令和gas语法,只需要产生一些东西,然后结束"fix" 它的几次迭代。就像检查寄存器+内存视图一样,发现 rcx 并没有改变,而是字符串数据被损坏了 => 换个方式试试...


哦,我完全忘了...您需要查找 64b 平台 ABI 的文档,这样您就知道将参数传递给 C 函数的正确方法是什么。

例如 linux 这些教程可能会有所帮助: http://cs.lmu.edu/~ray/notes/gasexamples/

并在此处搜索单词 "ABI" 以获取更多资源: https://whosebug.com/tags/x86/info