3

I'm trying to work out if a string is a palindrome or not in Assembly. Essentially I attempt to copy the bytes backwards from string 'my_string' to 'tmp_str'. Then I attempt to compare both strings use repe and cmpsb.

The problem I'm facing is that the program randomly hangs or throws SIGILL's OR SIGSEV's when setting breakpoints in GDB. The behaviour is truly random, or random enough that I can't the source of the error to a single line. I am incredibly confused and would appreciate some insight in what may be causing these errors. Note, if I set no break points or some others such as line 25, 27, 28, it works fine.

I have checked, and the for loop does iterate the correct number of times, with the right values. The initial strings are also in the format I expect.

Example errors:

Hangs <- if break point is set to line 30

Program received signal SIGILL, Illegal instruction. _start.for_1.end () at 4.asm:32 32 jmp .for_1.start <- when break point was set to line 28 (bug isn't reproducible but it did happen)

Program received signal SIGSEGV, Segmentation fault. 0x0000000000400feb in ?? () <- when break point was set to line 31

;; Write an assembly program to determine if a string stored in memory is a palindrome
;; (A palindrome is a string which is the same after being reversed, like "refer")
;; Use at least one repeat instruction

                segment .data
my_string       db              "bob", 0

                segment .bss
tmp_str         resb            4

                segment .text
                global _start

_start:
                ;/* Copy initial string BACKWARDS */
                xor             rcx, rcx                        ; have to use manual for loop
.for_1.start:
                cmp             ecx, 4
                je              .for_1.end
                mov             al, byte [my_string+ecx]        ; get character from original string from i'th place
                mov             rbx, tmp_str+2                  ; go to end of tmp_str (writing my_string to tmp_str backwards)
                sub             rbx, rcx                        ; we can't minus from address expression thingy, so just deduct from register stored above
                mov             [rbx], byte al                  ; copy byte in ebx into mem address stored atueax
                inc             ecx                             ; increment counter     
                jmp             .for_1.start                    ; unconditional start to stop (for loops check and do conditional jumps at top)
.for_1.end:
                ;/* Compare strings */
                mov             rsi, my_string                  ; now we want to compare strings (remember, pointer in reg moves in movsb)
                mov             rdi, tmp_str                    ; rdi, set to tmp_str address 
                mov             rcx, 2                          ; since rcx was modified by rep, we need to reset that though
                repe            cmpsb                           ; compare strings whilst each val (byte) is equal
                                                                ; now, if rcx is NOT 0, the bytes do not match and is not a palindrome!
                ;/* EOP */
                mov             eax, 1
                xor             ebx, ebx
                int 0x80

If anybody could offer some advice I would be greatly appreciative.

Compile commands: yasm -f elf64 -g dwarf2 <file>.asm ; ld <file>.o -o <file>

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Salih MSA
  • 120
  • 1
  • 7
  • 1
    `mov rbx, tmp_str+2` then `sub rbx, rcx` is odd, you should start with `rbx` pointing at the last byte. In the first iteration (`rcx` = 0) you want to point at `tmp_str + 3`, not plus 2. The comparison with the repeated compare string bytes instruction should use `mov ecx, 4` not 2. (Using `ecx` in this spot is an optimisation, `mov rcx, 4` would also be correct.) If you include the terminating NUL (zero) byte in the string (initial `ecx` loop runs for 4 iterations) you should skip it when comparing, ie `rsi` -> my_string with `rdi` -> tmp_str + 1. That's all I've found so far. – ecm Aug 22 '22 at 17:25
  • 2
    @ecm Why do you need to compare that many bytes? If you're checking whether a string is a palindrome by comparing it with a reversed copy of itself, isn't ⌊len/2⌋ bytes sufficient to check? – Joseph Sible-Reinstate Monica Aug 23 '22 at 02:57
  • 2
    What kernel and GDB versions? I don't see why this would ever SIGILL, but SIGSEGV is plausible for the bug Joseph identified about writing before the beginning of `.bss`. But only if it starts at the beginning of a page, not just following `.data` like it normally would. – Peter Cordes Aug 23 '22 at 03:00
  • 2
    Yes, compare the first half of the string with its 2nd half, total of n/2 compares, @JosephSible-ReinstateMonica. Of course it's not necessary to copy at all, just read from two pointers moving inwards from the start/end of the string towards the middle, until they cross. But this question is about why this particular code behaves weird under GDB, perhaps even varying from run to run with the same binary. – Peter Cordes Aug 23 '22 at 03:04
  • 1
    Also what `ld` version, and YASM version? When I try single-stepping this with `layout reg` (so register + source view), the debug info doesn't seem to match well, since I get two steps on the same source line sometimes (other than the `repe` where that's expected; I mean with RIP incrementing). I built with those commands using YASM 1.3.0, `ld` 2.38, GDB 12.1, on Linux 5.18 (Arch Linux) on bare metal (Skylake CPU). Using `b 30` to set a breakpoint there doesn't ever hit the breakpoint; it runs without crashing. – Peter Cordes Aug 23 '22 at 03:15
  • @PeterCordes YASM: yasm 1.3.0, Linux Kernel: 5.15.0-46-generic (Linux Mint 20.2, HP Omen with Intel i7 CPU) , ld: GNU ld (GNU Binutils for Ubuntu) 2.34, GDB: GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2 – Salih MSA Aug 23 '22 at 10:46
  • @PeterCordes long story short, the activity requested me to use REP* instruction variants - iterating backwards and forwards (by just for loop in the first half of my program) was my initial mental solution – Salih MSA Aug 23 '22 at 11:16
  • @JosephSible-ReinstateMonica you are absolutely correct. Once i fix this issue (if its fixable) I will make such changes. However this question was asked as I'm quite frankly perplexed that nothing ever ran consistently – Salih MSA Aug 23 '22 at 11:17
  • 1
    Ok, being forced to use `repe cmpsb` is the only reason it would ever make sense to do so here. It runs slow, and necessitates a bunch of useless work. (rep stos and rep movs have fast microcode implementations that can store or copy 32 or 64 bytes at a time for non-tiny inputs, but conditional repe / repne with scas/cmps are always slow, one element per 1 or 2 cycles on current CPUs, unfortunately. https://agner.org/optimize/ and see [Coaxing GCC to emit REPE CMPSB](https://stackoverflow.com/q/49375094) and [this answer](https://stackoverflow.com/a/55589634/224132) RE: it being slow. – Peter Cordes Aug 23 '22 at 11:22
  • @PeterCordes Sounds dreadful. Will keep that in mind – Salih MSA Aug 23 '22 at 11:49

2 Answers2

3

YASM is making bad DWARF2 debug info. It's old and unmaintained. Use NASM instead.

NASM 2.15.05 nasm -felf64 -g didn't work for me either: GDB 12.1 says there is no line 30 when I tried b 30. But still generally use NASM. I didn't try NASM's DWARF debug-info format; I've had problems with it in the past IIRC, like messing up objdump disassembly so it's probably not great.

Don't rely on debug info from NASM or YASM. Use layout asm and set breakpoints on numeric addresses, or at the current position that you single-step to. layout reg / layout n is a good way to a registers + disassembly view. You can copy/paste addresses from there or disas to do stuff like b *0x40101b. Start the program with starti so GDB stops before executing the first user-space instruction; from there you can si single-step by instruction. See the bottom of https://stackoverflow.com/tags/x86/info for asm debugging tips.

(Update: the NASM bug with debug info is described in GDB does not load source lines from NASM Will hopefully get fixed in a future version of NASM.)

Assembly language maps 1:1 with machine code, so it's actually helpful to look at canonical disassembly of it when debugging, may help you spot something where you wrote the wrong thing by accident.


When I build with YASM 1.3.0 and try single-stepping this with layout reg (so register + source view), the debug info doesn't seem to match well, since I get two steps on the same source line sometimes (other than the repe where that's expected; I mean with RIP incrementing).

I built with yasm -felf64 -gdwarf2 using YASM 1.3.0, ld 2.38, GDB 12.1, on Linux 5.18 (Arch Linux) on bare metal (Skylake CPU). Using b 30 to set a breakpoint there doesn't ever hit the breakpoint; it runs without crashing for me.


Debug info maps source lines to memory addresses, so setting a breakpoint in GDB modifies a byte of machine code other than the first of an instruction (to 0xcc INT3 software breakpoint).

This would lead to occasional illegal instructions, or more commonly to valid but different instructions (e.g. changing a byte of an absolute address), perhaps of shorter length leading to later bytes getting decoded as opcodes if a ModRM byte got modified. (Linux delivers SIGSEGV when user-space tries to run a privileged instruction, so various problems would all raise the same signal, even if the CPU exception was #GP rather than #PF). Also, overwriting a ModRM byte with 0xCC would change what the register operands are, so later instructions could use a bad register value.

0xCC as a ModRM byte is a register (not memory) operand with AH and CL or ESP and ECX. For example with the first 4 opcodes (add of different order and size), from putting db 0, 0xcc and so on into a .asm to make this example:

  401000:       00 cc                   add    ah,cl
  401002:       01 cc                   add    esp,ecx
  401004:       02 cc                   add    cl,ah
  401006:       03 cc                   add    ecx,esp

Imagine what would happen if any of your mov or sub instructions had their operands replaced with esp,ecx for example! (And if it happens to inc, it could actually change the instruction. Some of your mov-immediate instructions may have modrm bytes, too, since unlike NASM, YASM doesn't optimize mov rcx,2 to mov ecx,2; it uses mov r/m64, sign_extended_imm32.)

Or of course messing up the jmp rel8 would jump to the wrong place. (But CC is a negative 8-bit integer so it would jump backwards).

Using GDB to try to examine the situation (e.g. to disassemble the machine code that faulted) may not work, because GDB puts back the original machine code bytes for commands like x /i or disas to try to disassemble.

You might still see RIP pointing at a privileged instruction or a bad register value if an earlier 0xCC byte got decoding out of sync, but you wouldn't be able to see how execution of earlier instructions could have led to this point, because you wouldn't be seeing the earlier instructions that the CPU actually executed.


I can reproduce this, confirming stray 0xCC bytes

I was able to reproduce it by setting breakpoints on lines 29 and 31 as well as 30. When it segfaulted, RIP was 0x40103f, just past the end of the int 0x80.

Watching the disassembly view and single-stepping by instruction with si (stepi), execution went right through the repe cmpsb in one step, and through the int 0x80, faulting on a 00 00 add [rax],al after it.

mov rdi,0x402004 loaded 0x4020cc, the wrong address but still inside the same page. So the strings differed on the first byte, explaining why repe cmpsb ran only one instruction.

mov eax,0x1 loaded RAX with 0xcc. In the 32-bit int 0x80 ABI (which you normally don't want to use in 64-bit code, BTW) that's __NR_setregid32 (check asm/unistd_32.h). So int 0x80 returns with RAX=-1, -EPERM (asm-generic/errno-base.h).

In both these cases, a 0xcc byte was the 2nd byte of the instruction, the first byte of the immediate. x86 is little-endian, so that messed up the low byte of the value loaded.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • Okay, I will 1) finally use 64bit ABI irregardless of my tutorial book's lack of mention, 2) compile with NASM with recommended debug info – Salih MSA Aug 23 '22 at 10:56
  • By your answer, it appears your telling me the why of the issue (which I appreciate a lot, it's what I'm trying to learn), but not how to fix it. I made the changes stated above (to no avail), as I will still get segfaults and hangs. Is there a way to remedy this issue via a different version of kernel, gdb, etc. (versions mentioned in comment to my question) or am I stuck with the glitchy bug due to a really weird coincidence – Salih MSA Aug 23 '22 at 11:20
  • 1
    @SalihMSA: The top section of my answer, suggesting `nasm -felf64` which should help. If it doesn't, use numeric addresses for breakpoints (or `b` to set one at the current position after single-stepping). **If setting breakpoints on source line numbers doesn't work, don't do that.** With `layout asm` or `disas`, you can see the address of each instruction, so copy/paste it with the mouse to do `b *0x40101b` or whatever. Assembly language doesn't need debug info; the source maps 1:1 with machine code instructions. Start the program with `starti`, to stop before the first user-space insn. – Peter Cordes Aug 23 '22 at 11:26
  • Did the first thing (didn’t), forgot the second (numerical addresses as breaks). Can single step if needed. thank you! – Salih MSA Aug 23 '22 at 11:44
  • 1
    @SalihMSA: It's separate bugs in NASM and YASM, and/or disagreement with GDB over exactly how debug info works. If any versions of anything would matter, it's the assembler and GDB. Or *possibly* also the linker, if it has to do anything with the debug info like apply relocations based on final addresses. Updated my answer to not suggest that `nasm -g` would work; I tested and it doesn't for me either. I regularly use GDB on assembly code, but never with debug info because I'd *rather* see disassembly while debugging. – Peter Cordes Aug 23 '22 at 11:58
  • 1
    @SalihMSA: See [GDB does not load source lines from NASM](https://stackoverflow.com/a/73515239) re: its debug info. A patch is available so if you really wanted debug info for assembly, you could apparently get it. – Peter Cordes Aug 28 '22 at 00:41
2

The problem is that you're running your loop for one iteration too many, and so trying to put a copy of the NUL terminator from [my_string+3] into [tmp_str-1], which you're not supposed to write to. Change cmp ecx, 4 to cmp ecx, 3 and your program should then run correctly.

  • Thank you for your answer and yes that was an oversight I forgot to apply to that section. Regrettably however the program just hangs when a breakpoint is set to line 30. – Salih MSA Aug 23 '22 at 10:54