【问题标题】:Function that takes a char array and 2 indices; swapping the chars in those indices采用 char 数组和 2 个索引的函数;交换这些索引中的字符
【发布时间】:2020-10-08 18:24:15
【问题描述】:

这是我的函数原型:

char* swap(char* array, int index1, int index2);

这是我的汇编代码:

segment .text
   global swap

swap:
   mov r14,[rdi+rsi]
   mov r15,[rdi+rdx]
   
   mov [rdi+rsi],r15        ;this line segfaults
   mov [rdi+rdx],r14
   
   mov rax,rdi
   
   ret

mov [rdi+rsi],r15 mov [rdi+rdx],r14 行给了我一个段错误;我不知道我哪里错了

调用函数:

 #include <stdio.h>
 #include <stdlib.h>

 extern char* swapLetters(char* str, int indexA, int indexB);

 int main()
 {    
    char* st= "E X A M P L E";
    printf("Before swap: \t%s\n", st);

    char * res = swap(st, 2 ,10);

    printf("After swap: \t%s\n", res);
    return 0;
}

预期输出:

交换前:E X A M P L E

交换后:E L A M P X E

【问题讨论】:

  • 两个索引都在你的array中吗?
  • 我认为 mov r14[rdi+rsi] 会移动数组中索引 rsi 处的字符,我不太确定如何移动 1 个字符。非常感谢。
  • 我已经在编辑的问题中添加了C代码
  • 为什么不把c代码写出来编译一下看看生成的代码呢?有一些在线网站可以让你这样做
  • @Darthvader 使用汇编程序有什么意义? C 编译器生成它没有任何问题godbolt.org/z/G7eY9Y

标签: c assembly x86-64 nasm yasm


【解决方案1】:

主要问题是您的 st 变量被定义为指向字符串文字的指针。

char* st= "E X A M P L E";

String literalsC 语言中被视为只读。修改这样的字符串是未定义的行为。发生的事情是未知的,并且将特定于编译器及其运行的环境。当您在汇编代码中编写该内存时,您的环境会引发异常。在大多数使用现代编译器的现代操作系统上,字符串文字被放置在不可写的内存中,因此它会生成一个异常,这就是你的情况。

如果你想在可写内存中创建一个字符数组,你可以这样定义st

char st[] = "E X A M P L E";

汇编代码问题

一个问题是您对函数swap 的索引是int。在 64 位 GCC/CLANG 中,int 是 32 位。如果您将 32 位签名的 int 传递给汇编代码,则前 32 位可能包含垃圾。鉴于您的索引永远不会是负数,您应该使用无符号类型,最好是 64 位的。我会推荐 size_t 类型。 size_t 在 x86-64 代码中将是无符号的且大小为 64 位,因此当传递给汇编代码时,您无需在使用它们之前将索引值符号/零扩展为 64 位。我建议将swap 更改为:

char* swap(char* array, size_t index1, size_t index2)

如果您将 index1index2 保留为有符号整数 (int),则汇编代码的开头必须在 ESIEDXMOVSX /em> 注册。该代码如下所示:

swap:
    movsx rsi, esi        ; Sign extend 32-bit index1 parm in ESI to 64-bits
    movsx rdx, edx        ; Sign extend 32-bit index2 parm in EDX to 64-bits
    ; rest of function here

如果您将 32 位 unsigned int 用于 indexindex2,则必须将 32 位值扩展为零:

    mov esi, esi          ; Zero extend 32-bit index1 parm in ESI to 64-bits
    mov edx, edx          ; Zero extend 32-bit index2 parm in EDX to 64-bits
    ; rest of function here

当操作的目标是 64 位模式下的 32 位寄存器时,CPU 会自动将目标寄存器的高 32 位归零。将像 ESI 这样的 32 位寄存器移动到自身将清除 RSI 的高 32 位。这对所有通用寄存器都是一样的。


RBXRBPR12–R15 是根据x86-64 System V ABI 的非易失性寄存器。如果您的函数修改了它们,则必须保留它们的内容。您可以将它们压入堆栈并在完成后将它们的原始值从堆栈中弹出。首选方法是使用不需要保留的易失性寄存器之一,例如 R8-R11RAXRCXRDXRDIRSI


当您使用 64 位寄存器将数据移入/移出内存时,将传输 64 位(8 个字节)。举个例子:

mov r14,[rdi+rsi]

移动从内存地址[rdi+rsi]开始的8个字节,并将其移动到64位寄存器R14。稍后的写入会执行类似的操作,但会更新内存中的 8 个字节而不是 1 个字节。如果字符数组放在堆栈上,更新 8 个字节的数据可能会破坏堆栈,而您的代码和环境恰好就是这种情况。

当使用编号寄存器 R8R15 时,您可以通过在寄存器名称末尾添加 b 后缀来引用低 8 位 (w用于 16 位字,d 用于 32 位双字)。 64 位模式的 NASM/YASM 语法中所有寄存器名称的 complete chart 是:

mov r14,[rdi+rsi] 将被写为 mov mov r14b,[rdi+rsi] 以移动单个字节。您还必须对其他每个动作进行更改。


假设您将index1index2 更改为size_t(或uin64_t)类型,您的汇编代码可以写成:

segment .text
global swap

swap:
   push r14           ; Save non-volatile registers we overwrite
   push r15

   mov r14b,[rdi+rsi] ; Move one byte from [rdi+rsi] to R14B. R14B is lower 8 bits of R14
   mov r15b,[rdi+rdx] ; Move one byte from [rdi+rdx] to R15B. R15B is lower 8 bits of R15
   mov [rdi+rsi],r15b ; Move the byte in R15B to [rdi+rsi]
   mov [rdi+rdx],r14b ; Move the byte in R14B to [rdi+rdx]
   mov rax,rdi

   pop r15            ; Restore non-volatile registers
   pop r14
   ret

如果您要使用其他易失性寄存器而不是非易失性寄存器,则代码可以简化为:

segment .text
global swap

swap:    
   mov al,[rdi+rsi]   ; Move one byte from [rdi+rsi] to AL. AL is lower 8 bits of RAX
   mov cl,[rdi+rdx]   ; Move one byte from [rdi+rdx] to CL. CL is lower 8 bits of RCX
   mov [rdi+rsi],cl   ; Move the byte in CL to [rdi+rsi]
   mov [rdi+rdx],al   ; Move the byte in AL to [rdi+rdx]
   mov rax,rdi

   ret

在这种情况下,我们使用易失性寄存器的低 8 位 RAX(AL) 和 RCX(CL em>) 进行交换。由于我们不必保留这些寄存器,因此无需保存和恢复它们。

【讨论】:

  • asm 的最后一个问题:索引仅声明为int;允许调用者在高 32 位中留下垃圾,您可以通过传递更宽的整数变量的低 32 位来让 GCC 实际发出这样的调用者,其中完整的 64 位在调用之前用于某些事情。最好将它们声明为size_tuint64_t,否则在 asm 中您需要对两个输入进行movsxd 符号扩展。 (你可以用xchg esi, edx 对它们进行零扩展,代码量大,效率低。编译器当然会使用2x mov,可能mov esi,esi 会打败mov-elimination。)
  • 刚刚注意到 C 块声明了 swapLetters 的原型,而不是 swap,所以他们实际上称它为非原型,除非问题顶部的块也以某种方式被包含在内。 (像这样的 int 与 size_t 错误是为什么测试不足的一个很好的例子;使用常量整数参数,调用者传递它们的最简单方法是零扩展。所以我猜一个测试用例将指针指向 end 带有负参数的字符串会很好。或者 OP 可能有 Java 背景并且不知道unsigned,并且不打算专门签名。)
【解决方案2】:

这里的部分问题是一个不可写的内存区域被用来写入,它不会工作。 (asm 还有其他正确性问题,请参阅@MichaelPetch 的回答。)

创建时间:

char* st= "E X A M P L E";

因为它创建了一个string literal,所以指针st 指向一个不可写的内存位置。

如果创建为:

char st[] = "E X A M P L E";

st 存储在可写内存中,其内容字符,而不是仅仅持有一个指向只读字符串字面量的指针。

【讨论】:

  • 我恰好是你的支持者。可能有人投了反对票,因为这只是代码中的问题之一。我不确定。至少从 C 方面来看,您的答案似乎很好。
  • @MichaelPetch - 谢谢。一旦你得到接受,你就会从我这里得到一个。 (顺便说一句,我认为您应该这样做。)也感谢 dv 洞察力,当某些东西工作异常时,我确实指代程序集(通常在 Code::Blocks 环境中创建。)但除此之外,我不太熟悉它。跨度>
  • 我也没有投反对票,但很明显“函数采用具有 2 个索引的 char 数组”是在谈论函数 args。更清晰的措辞应该是“采用 char 数组 2个索引的函数”。
  • @PeterCordes - 我没有选择,但根据 OP 内容的上下文,我认为你是对的。谢谢你。 (不幸的是,我今天没时间了,我将不得不搁置它,直到我可以重新做一些事情。)
  • 别担心,通过删除最后一部分为您解决了这个问题,我将改写标题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-14
  • 1970-01-01
  • 2011-08-23
  • 2015-08-07
  • 2021-11-25
相关资源
最近更新 更多