2

im a beginner at assembly, and im trying to write an assembly function that implements the largestdif function in this piece of c code (it receives 3 integers as arguments). it basically needs to find the minimum and maximum of the 3 arguments and then return the subtraction between the maximum and minimum:

#include <stdio.h>
extern int largestdif( int n1, int n2, int n3 );
int main( ) {
printf( "largestdif: %d\n", largestdif( 100, 30, 10 ) );
printf( "largestdif: %d\n", largestdif( 0, -1, 1 ) );
return 0;
}

heres my piece of code in assembly, looks kinda confusing but im hoping you can tell me whats wrong with this since its presenting segmentation fault error. i really have no idea whats left to do, it must have something to do with the registers not being called enough. cant seem to grasp an answer so yeah im hoping someone could give me a hand. thanks in advance.

.global largestdif
.text
largestdif:
push %ebp
mov %esp, %ebp
mov 8(%ebp), %ebx
push %ebx
mov 12(%ebp), %ecx
push %ecx
mov 16(%ebp), %edx
push %edx

#compare n1 and n2 to find the largest number
cmp %edx, %ecx
jl n1_menor_n2
    #block else
    cmp %edx, %ebx #compare first and third numbers
    jl firstlower
        #bloco else
        mov %edx, %eax #already have largest number
        jmp continue
    firstlower:
        mov %ebx, %eax #already have largest number
        jmp continue
n1_menor_n2:
    cmp %ecx, %ebx #compare second and third numbers
    jl secondlower
        #block else
        mov %ecx, %eax # already have largest
        jmp continue
    secondlower:
        mov %ebx, %eax # already have largest
        jmp continue


continue: 
#compare n1 and n2 to find the largest number
cmp %edx, %ecx
jg n1_maior_n2
    #block else
    cmp %edx, %ebx
    jg firstlarger
        #block else
        sub %edx, %eax
        jmp continue2
    firstlarger:
        sub %ebx, %eax
        jmp continue2

n1_maior_n2:
    cmp %ecx, %ebx
    jg secondlarger
        #block else
        sub %ecx, %eax
        jmp continue2

    secondlarger:
        sub %ebx, %eax #already have the subtraction 
        jmp continue2

continue2:
    pop %edx
    pop %ecx
    pop %ebx
    mov %ebp, %esp
    pop %ebp
    ret
mrsc
  • 21
  • 2
  • 2
    Use your debugger to at least pinpoint the faulting instruction. Then work backwards to see how that happened. In any case, pushing `ebx` **after** you changed it is not going to do much good. – Jester May 08 '18 at 23:11
  • problem is i dont know how to use it – mrsc May 08 '18 at 23:20
  • You should do yourself a favor and figure out how to use your platform's debugger. There are almost certainly easily-accessible tutorials for every variant of "debug assembly on [platform]". – zneak May 09 '18 at 02:30
  • 1
    even though it means you will continue on past the loop, continue is a horrible name to end your loop as it usually means go back to the top of the loop – technosaurus May 09 '18 at 06:15
  • 2
    Write a C implementation for `largestdiff`, and compile with assembly output. First without optimization, and then with optimization. I think you'll find the code generated by the compiler to be far more instructive if you're willing to step through it. – Brett Hale May 09 '18 at 08:14

2 Answers2

1

What your code is doing:

if (n2 < n1) {
    if (n3 < n2) min = n3;
    else         min = n2;
} else {
    if (n3 < n1) min = n3;
    else         min = n1;
}

if (n2 > n1) {
    if (n3 > n2) return min - n3;
    else         return min - n2;
} else {
    if (n3 > n1) return min - n3;
    else         return min - n1;
}

As you've likely realized, this approach is needlessly complicated in x86 ASM, regardless of the syntax you use. The C code even shows it!

Instead, it is more efficient to use default values for min and max and compare the non-default values against min and max, then return the result of subtraction:

int min = n1, max = n3;

if (min > n2) min = n2;
if (min > n3) min = n3;

if (max < n1) max = n1;
if (max < n2) max = n2;

return max - min;

Same number of required comparisons, but notice the elimination of the else branches, which makes the code simpler. Translated to some simple assembly code:

    .text
    .globl largestdif
largestdif:
    pushl %ebp
    movl %esp, %ebp

    movl 16(%ebp), %edx  # d = n1, min = n1
    movl 12(%ebp), %ecx  # c = n2
    movl 8(%ebp), %eax   # a = n3, max = n3

# Find min.
    cmpl %ecx, %edx      # if (n1 <= n2) {}
    jle .min_not_n2      # else
    xchgl %ecx, %edx     #   swap(&n1, &n2) // n1 < n2 is true after this
.min_not_n2:
    cmpl %eax, %edx      # if (n1 <= n3) {}
    jle .min_not_n3      # else
    xchgl %eax, %edx     #   swap(&n1, &n3) // n1 < n3 is true after this
.min_not_n3:

# Find max.
    cmpl %ecx, %eax      # if (n3 >= n2) {}
    jge .max_not_n2      # else
    xchgl %ecx, %eax     #   swap(&n2, &n3) // n3 > n2 is true after this
.max_not_n2:

    # n1 > n3 is absolutely false at this point.
    # The swaps above ordered things into the relation
    # n1 <= n2 <= n3 (%edx <= %ecx <= %eax),
    # so n1 <= n3 must be true.

# Return max - min.
    subl %edx, %eax

    movl %ebp, %esp
    popl %ebp
    ret

I did my best to optimize it, but I'll admit it's almost certainly possible to optimize this code even further.

  • [`xchg` costs about as much as 3 `mov reg,reg` instructions on Intel CPUs](https://stackoverflow.com/questions/45766444/why-is-xchg-reg-reg-a-3-micro-op-instruction-on-modern-intel-architectures) with the added disadvantage of not benefiting from mov-elimination. But in 32-bit code where you only have 3 call-clobbered registers, you'd have to save/restore an extra scratch reg (or reload an arg from memory) to use `mov`. Your version is nice and compact, though, so (other than unnecessary EBP-stack-frame stuff) it's pretty well optimized for code-size and simplicity. – Peter Cordes May 12 '18 at 19:16
  • Another approach could be to form all 3 differences, and take the largest absolute value. That could be efficient with SSE4.1 for `pmaxd` and `pabsd`. Yeah, maybe load the args with `movdqu`, make a shuffled copy with `pshufd`, then `psubd` / `pabsd` and do some shuffle / `pmaxd` to get the max of the 3 non-garbage elements. But if the return value is supposed to be unsigned, we can't support as large a range of differences as we can by finding the min and max and then subtracting, I don't think. – Peter Cordes May 12 '18 at 19:21
0

No segmentation fault here. I guess the main mistake is not to observe the calling convention. The program is essentially a C program. So you have to follow the C calling convention "cdecl". The function largestdif has to return the registers EBX, ESI, EDI, EBP unchanged. If you use these registers in a function, you have to preserve them before changing them and restore them before leaving the function.

You didn't preserve EBX before changing it. Change

...
mov 8(%ebp), %ebx
push %ebx
...

to

...
push %ebx
mov 8(%ebp), %ebx
...
rkhb
  • 14,159
  • 7
  • 32
  • 60
  • well i did that but the result is now showing with the wrong sign (-90 instead of 90 and -2 instead of 2) – mrsc May 09 '18 at 16:37
  • 1
    @mrsc: I know, but I thought that was intended. I guess you didn't consider that in AT&T syntax everything is twisted. A `cmp %edx, %ecx` equates to `cmp ecx, edx` in Intel syntax. Thus, a `JL` jumps if ECX is lower than EDX. And a `sub %edx, %eax` subtracts EDX from EAX. BTW, `n1` is in `%ebx`, not in `%edx`. You need to use a debugger to check if the conditional jumps work as expected. – rkhb May 09 '18 at 17:44