I've broken down the disassembly for you to show how the assembly was produced from the C source.
8(%ebp) = x, 12(%ebp) = y, 16(%ebp) = z
arith:
Create the stack frame:
pushl %ebp
movl %esp,%ebp
xeaxyedxmovl 8(%ebp),%eax
movl 12(%ebp),%edx
t1 = x + ylealedxeaxt1ecxleal (%edx,%eax),%ecx
int t4 = y * 48;t4edxMultiply edx by 2, and add edx to the result, ie. edx = edx * 3:
leal (%edx,%edx,2),%edx
Shift left 4 bits, ie. multiply by 16:
sall $4,%edx
int t2 = z+t1;ecxinitiallyt1z16(%ebp)ecxt2addl 16(%ebp),%ecx
int t5 = t3 + t4;t3x + 4t3t3(x+4) + t4t3t4edxt4eaxxoffsetleal 4(%edx,%eax),%eax
int rval = t2 * t5; Fairly straight-forward this one; ecx represents t2 and eax represents t5. The return value is passed back to the caller through eax.
imull %ecx,%eax
espebpmovl %ebp,%esp
popl %ebp
ret
As others have said, you can't go exactly back to the source from the disassembly. It's up to the interpretation of the person reading the assembly to come up with equivalent C code.
Compile with debug information (-g), which will embed the source:
gcc -c -g arith.c
If you're on a 64-bit machine, you can tell the compiler to create a 32-bit binary with the -m32 flag (I did so for the example below).
Use objdump to dump the object file with it's source interleaved:
objdump -d -S arith.o
-d = disassembly, -S = display source. You can add -M intel-mnemonic to use the Intel ASM syntax if you prefer that over the AT&T syntax that your example uses.
Output:
arith.o: file format elf32-i386
Disassembly of section .text:
00000000 <arith>:
int arith(int x, int y, int z)
{
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 20 sub $0x20,%esp
int t1 = x+y;
6: 8b 45 0c mov 0xc(%ebp),%eax
9: 8b 55 08 mov 0x8(%ebp),%edx
c: 01 d0 add %edx,%eax
e: 89 45 fc mov %eax,-0x4(%ebp)
int t2 = z+t1;
11: 8b 45 fc mov -0x4(%ebp),%eax
14: 8b 55 10 mov 0x10(%ebp),%edx
17: 01 d0 add %edx,%eax
19: 89 45 f8 mov %eax,-0x8(%ebp)
int t3 = x+4;
1c: 8b 45 08 mov 0x8(%ebp),%eax
1f: 83 c0 04 add $0x4,%eax
22: 89 45 f4 mov %eax,-0xc(%ebp)
int t4 = y * 48;
25: 8b 55 0c mov 0xc(%ebp),%edx
28: 89 d0 mov %edx,%eax
2a: 01 c0 add %eax,%eax
2c: 01 d0 add %edx,%eax
2e: c1 e0 04 shl $0x4,%eax
31: 89 45 f0 mov %eax,-0x10(%ebp)
int t5 = t3 + t4;
34: 8b 45 f0 mov -0x10(%ebp),%eax
37: 8b 55 f4 mov -0xc(%ebp),%edx
3a: 01 d0 add %edx,%eax
3c: 89 45 ec mov %eax,-0x14(%ebp)
int rval = t2 * t5;
3f: 8b 45 f8 mov -0x8(%ebp),%eax
42: 0f af 45 ec imul -0x14(%ebp),%eax
46: 89 45 e8 mov %eax,-0x18(%ebp)
return rval;
49: 8b 45 e8 mov -0x18(%ebp),%eax
}
4c: c9 leave
4d: c3 ret
As you can see, without optimizations the compiler produces a larger binary than the example you have. You can play around with that and add a compiler optimization flag when compiling (ie. -O1, -O2, -O3). The higher the optimization level, the more abstract the disassembly's going to seem.
For example, with just level 1 optimization (gcc -c -g -O1 -m32 arith.c1), the assembly code produced is a lot shorter:
00000000 <arith>:
int arith(int x, int y, int z)
{
0: 8b 4c 24 04 mov 0x4(%esp),%ecx
4: 8b 54 24 08 mov 0x8(%esp),%edx
int t1 = x+y;
8: 8d 04 11 lea (%ecx,%edx,1),%eax
int t2 = z+t1;
b: 03 44 24 0c add 0xc(%esp),%eax
int t3 = x+4;
int t4 = y * 48;
f: 8d 14 52 lea (%edx,%edx,2),%edx
12: c1 e2 04 shl $0x4,%edx
int t5 = t3 + t4;
15: 8d 54 11 04 lea 0x4(%ecx,%edx,1),%edx
int rval = t2 * t5;
19: 0f af c2 imul %edx,%eax
return rval;
}
1c: c3 ret