I made a Brainfuck interpreter in assembly (AT&T syntax, GCC). It works on most stuff. All the desired actions work (+ - > <). I made sure to take care of nested loops as well (pushing the address on the stack and after exiting the loop popping it off). And I used syscalls for writing and reading.
Using this code:
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
It prints Hello World! without an issue, notice how it pretty much has everything you need: Nested loops, all the commands you can do (except ,). And it still works perfectly fine.
However when using this code (The expected output is also Hello World! just like the first code):
--<-<<+[+[<+>--->->->-<<<]>]<<--.<++++++.<<-..<<.<+.>>.>>.<<<.+++.>>.>>-.<<<+.
It prints "n| 1f14 {g" (without any seg fault or illegal instruction). I'm scratching my head on why this code doesn't work while the other one works perfectly fine. I thought I took care of every possible instruction and some exceptions which have to be hard coded (nested loops). Is there some other caveat I should keep in mind when writing a Brainfuck interpreter? Nested loops seemed like the only caveat and those work perfectly fine. I've tried debugging but it didn't help me either.
This is my code:
.global main
format_str: .asciz "--<-<<+[+[<+>--->->->-<<<]>]<<--.<++++++.<<-..<<.<+.>>.>>.<<<.+++.>>.>>-.<<<+."
main:
pushq %rbp
movq %rsp, %rbp
movq $format_str, %rdi
call brainfuck
movq $0, %rdi
popq %rbp
call exit
brainfuck:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
movq $1, %r15 # use as loop counter
movq %rdi, %rbx # move the pointer to the first char in the string to %rbx
movq $100000, %rdi # allocate 100000 bytes in the heap
call malloc # call the actual malloc function
movq %rax, %r12 # move the data pointer to the allocated bytes to r12
addq $50000, %r12 # add 50000 to the location so we start in the middle. So when starting you can also go to "negative" cells
sub $1, %rbx
.back:
add $1, %rbx # iterate one char over the string we have
.findVal:
movzbl (%rbx),%eax # move (%rbx) into %eax, only accesses memory once, speeding up the tests.
cmp $0,(%rbx) # test for end of string
je .end
cmp $'+', %al # test for plus
je .plus
cmp $'-', %al # test for minus
je .minus
cmp $'>', %al # test for right
je .right
cmp $'<', %al # test for left
je .left
cmp $'[', %al # test for opening bracket
je .openloop
cmp $']', %al # test for closing bracket
je .closeloop
cmp $'.', %al # test for dot
je .dprint
cmp $',', %al # test for comma
je .cinput
jmp .back # if none match then it is an unsupported command, so iterate once over the string and jump back
.plus:
add $1,(%r12) # add 1 to the cell the data pointer (r12) is pointing at
jmp .back
.minus:
sub $1,(%r12) # subtract 1 of the cell the data pointer (r12) is pointing at
jmp .back
.right:
add $1,%r12 # add 1 to the memory location of the data pointers, (hence moving up 1 cell)
jmp .back
.left:
sub $1,%r12 # subtract 1 to the memory location of the data pointers, (hence moving down 1 cell)
jmp .back
.openloop:
cmpb $0,(%r12) # if the current cell we are pointing at is 0, jump to closing bracket
je .jumpclose
pushq %rbx # save the opening bracket location on the stack (to deal with nested loops)
jmp .back
.closeloop:
cmpb $0,(%r12) # if the current cell we are pointing at is 0, exit the loop
je .endloop
movq (%rsp), %rbx # if it's not 0, move the top of the stack to the rbx, so we start back at the last pushed opening bracket
jmp .back
.endloop:
addq $8,%rsp # if it is the end of a loop (the cell at (%r12) is 0), then we add 8 to the stack pointer, so that the next jumping position (if there is one) is at (%r12)
jmp .back
.inc_r15:
add $1, %r15 # increase nested loop counter
.jumpclose: # finds the corresponding closing bracket
add $1, %rbx # go to next char
movzbl (%rbx),%eax # move rbx to eax so we access memory less often
cmp $']', %al # check for closing loop
je .closeloopfound
cmp $'[', %al # check for opening loop
je .inc_r15 # if there is another nested loop increase the loop counter
jmp .jumpclose # if none of those then simply jump back to iterate to the next char
.closeloopfound:
sub $1, %r15 # when encountering a closing loop, subtract 1 from the loop counter
cmpq $0, %r15 # r15 is intiated with 1 since when we enter a loop we also have 1 opening bracket, if its equal to 0 after the last subtraction that means we found the correct bracket
jne .jumpclose # if it's not 0, then jump back to jumpclose to continue finding the correct bracket
mov $1, %r15 # move 1 back into r15 to take care of the next time we have to skip a loop
jmp .back # jump back
.dprint:
movq $1, %rax # perform syscall 1 which is sys_write
movq $1, %rdi # write to stdout
movq %r12, %rsi # use the char that %r12 is pointing to, which is stored as ascii
movq $1, %rdx # write 1 byte (amount of byte)
syscall # perform the system_write (print) with syscall
jmp .back
.cinput:
movq $0, %rax # sys_read call number
movq $1, %rdi # read from stdin
movq %r12, %rsi # print the ascii value of whatever %r12 is pointing to
movq $1, %rdx # write 1 char
syscall # perform the system_read with syscall
jmp .back
.end: # exiting the function
popq %rbx # restore %rbx
popq %rbp # restore %rbp
ret # return to the proper return adress
I added some comments for clarity. I am just completely lost where the code goes wrong, I am using an online compiler which never failed me before. So I don't think that's the issue.
EDIT: After some more searching I found out a "caveat" I missed. I am not checking for 0 when entering a loop, I am merely checking for 0 when exiting a loop, like a true dumb ass. I've updated my code and added this functionality, luckily some programs do work now (Like Sierpinski from Daniel Cristofani). But some programs still fail sadly (the program above still fails). Now I really have no idea what to do anymore.
.back:just above the dispatch code. Special-case the first iteration by jumping over it or doingdec %rbxto cancel out the add, instead of having an extrajmpon every iteration. IDK what your bug might be; you did try single-stepping this in a debugger, right? Look for something that goes wrong earlier than the final result.subq $1,(%r12)is an 8-byte sub; your cells are 1 byte so you're allowing carry-out to propagate between cells. That's a bug, IDK if it's the cause of the problem you're asking about. Also related Brainfreeze: A Brainfuck compiler in C on CodeReview talks some about x86 asm sequences that implement BF "instructions". JITing vs. dispatch with a chain of branches isn't a big difference as far as what the blocks need to be. (And BTW, for performance you might want to dispatch via ajmp *table(%rax)after a movzx load.)