clinuxassemblynasmstrcmp

Implement strcmp function in assembly 64 on linux


I am trying to implement strcmp which is a C function in assembly 64, here is my working code so far :

global ft_strcmp

section .text

ft_strcmp:              ;rax ft_strcmp(rdi, rsi)
        mov     r12, 0  
loop:
        mov r13b, [rdi + r12]
        cmp byte [rdi + r12], 0
        jz exit
        cmp byte [rsi + r12], 0
        jz exit
        cmp r13b, byte [rsi + r12]
        jnz  exit
        inc r12
        jmp loop

exit:
        sub r13b, [rsi + r12]
        movsx rax, r13b
        ret 

when I try to compile it with this main.c:

#include <stdio.h>
#include <string.h>

int     ft_strcmp(const char *str1, const char *str2);

int     main()
{
        const char      str1[20] = "hella world";
        const char      str2[20] = "hello world";

        printf("ft_strcmp = %d\n",ft_strcmp(str1, str2));
        printf("strcmp = %d\n",strcmp(str1, str2));
   
        return (0);
}

the following result is as shown below :

ft_strcmp = -14
strcmp = -14

which is the result of the subtraction of o from a : ret = 'a' - 'o' which is in decimal ascii code 97 - 111 = -14.

but when I try it with another main.c as below, I just passed strings directly to both strcmp() and ft_strcmp() rather than passing declared variables:

#include <stdio.h>
#include <string.h>

int     ft_strcmp(const char *str1, const char *str2);

int     main()
{
        printf("ft_strcmp = %d\n",ft_strcmp("hella world", "hello world"));
        printf("strcmp = %d\n",strcmp("hella world", "hello world"));
        return (0);
}

the results becomes:

ft_strcmp = -14
strcmp = -1

I searched a little bit in this wide Internet and I found some explanations about this behavior:

Why does strcmp() in a template function return a different value?

Is this the only return value for strcmp() in C?

but the question is how I implement this behavior in my assembly code, I mean is there a way to know if the string is passed directly to the parameters, or not?

I tried to debug a little bit with lldb and I found the addresses of rdi and rsi (the registers that get the first parameter and the second parameter respectively) are different in the above two cases.

in the first case the addresses are written like this :

rdi = 0x00007fffffffde50  ; the address of the first string
rsi = 0x00007fffffffde70  ; the address of the second string

but int the second case they are written like this:

rdi = 0x0000555555556010  ; the address of the first string
rsi = 0x0000555555556004  ; the address of the second string

I am not sure if this will help or not, but who knows, and thanks in advance.

#Edit

Well since my question marked as [duplicate], I will post my answer that it seems to do the job of the above behavior, and it is as follows:

after debugging using lldb I noticed whenever I pass a literal string to ft_strcmp(), the address of rdi and rsi are written like this:

rdi = 0x0000555555556010  ; the address of the first string
rsi = 0x0000555555556004  ; the address of the second string

and whenever I pass declared variables instead of literal strings, the addresses become like this:

rdi = 0x00007fffffffde50  ; the address of the first string
rsi = 0x00007fffffffde70  ; the address of the second string

"at least this is what I got on my machine with linux X64 operating system", so I thought about doing some shifting tricks:

this is how 0x00007fffffffde50 is represented in binary:

 11111111111111111111111111111111101111001010000

I will shift it with 44 bits in order to get that 7 to use it in comparison later, let's store it in rax register in this example:

mov rax, 0x00007fffffffde50
rax >> 44  in assembly ==> shr  rax, 44 ==> (rax = 111 ==> 7)

and now I will check if rdi and rsi are literal strings or not :

mov r8, rdi       ; store the address of rdi in r8
shr r8, 44        ; right shift the address of r8 by 44 bits
cmp r8, rax       ; compare if the results are the same or not
jl  loop2         ; if r8 < rax then jump to loop2 for example 5 < 7

and here is my final code, but I am not sure if this is a good way or not, it is just a small trick that it works with me with the above tests, not sure about complicated test. (NOTE: It will not work with calling variables that declared at the global scope, thanks to Peter Cordes for spotting out that)

global ft_strcmp

section .text

ft_strcmp:      ;rax ft_strcmp(rdi, rsi)
    mov r12, 0  
    mov rax, 0x00007fffffffde50
    shr rax, 44
    mov r8, rdi
    shr r8, 44
    cmp r8, rax
    jl  loop2
loop1:
    mov r13b, [rdi + r12]
    cmp byte [rdi + r12], 0
    jz exit1
    cmp byte [rsi + r12], 0
    jz exit1
    cmp r13b, byte [rsi + r12]
    jnz  exit1
    inc r12
    jmp loop1

exit1:
    sub r13b, [rsi + r12]
    movsx rax, r13b
    ret
loop2:
    mov r13b, [rdi + r12]
    cmp byte [rdi + r12], 0
    jz exit2
    cmp byte [rsi + r12], 0
    jz exit2
    cmp r13b, byte [rsi + r12]
    jnz  exit2
    inc r12
    jmp loop2

exit2:
    cmp r13b, byte [rsi + r12]
    jl ret_m
    jg ret_p
ret_z:
    mov rax, 0
    ret
ret_p:
    mov rax, 1
    ret
ret_m:
    mov rax, -1
    ret

and now the results are the same when I compile with the both main.c above.


Solution

  • strcmp() only guarantees the sign of the result. Something probably got optimized in the second case. You don't need to care that the magnitude is different, so it would be best if you didn't.

    The compiler would be within its rights to optimize

    printf("strcmp = %d\n",strcmp("hella world", "hello world"));
    

    to

    printf("strcmp = %d\n",-1);