First off, you have nice, clean, well-formatted, easy-to-read code. You have even included comments that explain the goal of each instruction. Too much of the time I review assembly-language code, these are the things that go wrong. You've gotten them all correct. Nice job! Now I don't have to pull my hair out trying to read and understand your code.
Unfortunately, were I your instructor, I'd still have to give you a failing score on the assignment because you didn't follow the rules. The assignment says that you must "Do it without actually adding the numbers.", but right off the bat, you ADD
the input values together. And we were off to such a good start… :-(
Now, personally, I think these types of assignments are rather silly, so I wouldn't be giving them. If I wanted you to learn how to use the bitwise operators, I'd find something useful and real-world that they are good for, and then give you that assignment. It's not like I'd have to work very hard. The chip designers didn't put them in merely for fun.
Oh well; you have to do the assignment that you were given. So follow the hint, use XOR
. Maybe you don't know exactly what that means, so what I'd do is open up a programmer's calculator (all major desktop operating systems have a "programmer" mode for their calculators) and play around with it. Pick random combinations of positive and negative numbers, and compare what the results are when you add them together versus when you XOR them together. Try to get a feel for what XOR does. Then, look up a formal definition of XOR (exclusive-OR). If you're like me, your eyes glaze over at the symbolic logic stuff (which wasn't so great when I took that course in college); feel free to skip over that for the purposes of this assignment. Your real goal here is to find out what XOR actually does at the bit level. There are lots of detailed explanations of bitwise manipulation online, or you may have a textbook that covers this stuff, too.
For example:
01101101
XOR 11010100
_____________
10111001
Notice that each column follows the truth table for an XOR operation, which basically just says that the output is 1 (true) whenever the inputs differ. That's the "exclusive" part of the OR.
Eventually, while doing this preliminary research, I suspect you'd come across someone talking about how XOR operations are used to implement parity checks. Mast's answer here already spilled the beans. Parity indicates whether an integer is even or odd, and can be calculated simply by a XOR sum of the bits. Judging from your comments in the code, you already know that for a binary number, the lowest (least significant) bit determines whether it is odd or even.
So the good news is that your code is almost entirely correct. If you change the ADD
s to XOR
s, it will follow the rules of the assignment and produce the correct result. Thus, you would have:
call read_hex ; Provided by the teacher. Reads a hexadecimal number from stdin.
mov ebx, eax
call read_hex
xor ebx, eax
call read_hex
xor ebx, eax
Now we've XOR-summed all of the bits from the three inputs. All that's left is figuring out the parity of the result—i.e., whether the XOR-sum is odd or even.
You've already implemented one way of doing that, but as Quuxplusone pointed out, it is an unnecessarily complicated way. While it doesn't always hold when you start getting into more advanced things, for simple arithmetic operations, fewer instructions means faster code. Arguably more importantly, it means simpler code, which is more likely to be correct code. Moreover, fewer branches virtually always mean faster code, and certainly code whose flow of execution is easier to follow, and thus easier to debug.
I'd disagree slightly with Quuxplusone here and say that clever is totally fine, as long as your cleverness has some notable advantage. You don't always have to write code the "normal" way, because the "normal" way might be sub-optimal. Generally, if we're dropping down to write in assembly, it's because we want to write the best code we possibly can (either fastest, shortest, or whatever metric we are using to judge "best"), which means that "normal" isn't necessarily an important goal. Sometimes, "readable" isn't even an important goal. But, by the same token, I do agree there's no point in deviating from what is normal if your deviation is inferior, and that's certainly the case here.
Your XOR-sum is in the EBX
register. You know that the XOR-sum tells you the parity in the least-significant bit. So what is the obvious thing to do? Mask off everything but the least-significant bit, and that'll be your answer! How do we mask off bits? Use a logical AND operation:
and ebx, 1 ; mask off all but the low-order bit
Oh, and finally, since we need the result in EAX
, we'll do:
mov eax, ebx
(We could have just as easily done the MOV
first, before the AND
. It doesn't matter.)
Go through your current code, and prove to yourself that this gives exactly the same results, without needing to flip the bit with the XOR
, without needing to involve the carry flag (CF) via ROR
, and without needing to do any conditional branching (JC
).
Putting it all together, the code is essentially:
call read_hex ; Provided by the teacher. Reads a hexadecimal number from stdin into EAX.
mov ebx, eax
call read_hex
xor ebx, eax
call read_hex
xor ebx, eax
mov eax, ebx ; Move XOR-sum of three given numbers into EAX.
and eax, 0x1 ; Mask off all but the lowest-order bit.
call print_eax_binary ; Provided by the teacher. Prints a binary number in EAX to stdout.
; Exit the process:
push 0
call [ExitProcess]
Simpler, clearer, faster. Rarely is it possible to say this about any piece of code, but I believe this really is the most optimal way to write this.