Addressing of instruction pointer in Mac OS X x86-64 - c

I wanted to understand a litte more about assembly and wrote a little example:
#include <stdio.h>
#include <math.h>
void f() {
unsigned char i[4];
i[0] = 5;
i[1] = 6;
i[2] = 7;
i[3] = 8;
int j = 0;
for(j=0; j < 20; j++)
printf("%02X\n", i[j]);
}
int main() {
int i[5];
i[0] = 3;
i[1] = 3;
i[2] = 3;
i[3] = 3;
i[4] = 3;
f();
return 0;
}
My goal was to see the actual return address for the instruction pointer, laid down by the call to
callq in main(), when it started f().
I used gdb to disassemble main() and got the following
Dump of assembler code for function main:
0x0000000100000eb0 <main+0>: push %rbp
0x0000000100000eb1 <main+1>: mov %rsp,%rbp
0x0000000100000eb4 <main+4>: sub $0x20,%rsp
0x0000000100000eb8 <main+8>: movl $0x3,-0x1c(%rbp)
0x0000000100000ebf <main+15>: movl $0x3,-0x18(%rbp)
0x0000000100000ec6 <main+22>: movl $0x3,-0x14(%rbp)
0x0000000100000ecd <main+29>: movl $0x3,-0x10(%rbp)
0x0000000100000ed4 <main+36>: movl $0x3,-0xc(%rbp)
0x0000000100000edb <main+43>: callq 0x100000e40 <f>
0x0000000100000ee0 <main+48>: movl $0x0,-0x8(%rbp)
0x0000000100000ee7 <main+55>: mov -0x8(%rbp),%eax
0x0000000100000eea <main+58>: mov %eax,-0x4(%rbp)
0x0000000100000eed <main+61>: mov -0x4(%rbp),%eax
0x0000000100000ef0 <main+64>: add $0x20,%rsp
0x0000000100000ef4 <main+68>: pop %rbp
0x0000000100000ef5 <main+69>: retq
so i was expecting to find the laid down instruction pointer return address to be 0x0000000100000ee0, as this is the next instruction after callq. When I run my program I get ( I grouped these in groups of 4 so you can read them better):
05
06
07
08
40
1B
08
56
FF
7F
00
00
E0
EE
B7
09
01
00
00
00
00
00
00
00
03
00
00
00
03
00
00
00
03
00
00
00
03
00
00
00
Ok, so I can see my 5,6,7,8 that I wrote into my local variable in f() and I can see the local variables of main() those 4-byte integers, which have been set to 3. After 5,6,7,8 (this is a 64 bit system) I would have expected the next 8 bytes to encode the previous value of the %rbp register, and THEN the
next 8 bytes to contain the return address for the instruction pointer. So the return address should be
E0
EE
B7
09
01
00
00
00
Now when I compare this to the 0x0000000100000ee0 that I am expecting from gdb, I can see the 00000001 in the last 4 bytes and I can see the e0 from 00000ee0 in the very first byte. But why am I not getting exactly what I am expecting? I thought about byte-ordering (Mac OS X is little endian I believe), but that would not explain what I see here, from what I understood.
Any input is welcome,
Thank you guys,
Christoph

Try this program and run it multiple times.
#include <stdio.h>
int
main(int argc, char **argv)
{
int foo;
printf("%p %p\n", main, &foo);
return 0;
}
I'm pretty sure that you'll get different addresses every time. MacOS has position independent binaries and the stack changes positions all the time too. This is a security feature.
If you run your program in gdb, you'll probably get what you expect since gdb disables the randomization to make debugging easier.

Related

Buffer overflow , stack pointer manipulation using GDB

I have a simple problem in c which may be solved using GDB, but I am not able to solved it.
We have a main() function which calls another function, say A(). When function A() executes and returns, instead of returning to main() it goes to another function, say B().
I don't know what to do in A() so that return address will change.
Assuming, the OP wants to force a return from A() to B() instead of to main() from where A() was called before...
I always believed to know how this might happen but never tried by myself. So, I couldn't resist to fiddle a bit.
Manipulation of return can hardly be done portable as it exploits facts of the generated code which may depend on compiler version, compiler settings, platform, and whatever.
At first, I tried to find out some details about coliru which I planned to use for fiddling:
#include <stdio.h>
int main()
{
printf("sizeof (void*): %d\n", sizeof (void*));
printf("sizeof (void*) == sizeof (void(*)()): %s\n",
sizeof (void*) == sizeof (void(*)()) ? "yes" : "no");
return 0;
}
Output:
gcc (GCC) 8.2.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
sizeof (void*): 8
sizeof (void*) == sizeof (void(*)()): yes
Live Demo on coliru
Next, I made a minimal sample to get an impression about the code which will be generated:
Source code:
#include <stdio.h>
void B()
{
puts("in B()");
}
void A()
{
puts("in A()");
}
int main()
{
puts("call A():");
A();
return 0;
}
Compiled with x86-64 gcc 8.2 and -O0:
.LC0:
.string "in B()"
B:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC0
call puts
nop
pop rbp
ret
.LC1:
.string "in A()"
A:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC1
call puts
nop
pop rbp
ret
.LC2:
.string "call A():"
main:
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC2
call puts
mov eax, 0
call A
mov eax, 0
pop rbp
ret
Live Explore on godbolt
On Intel x86/x64:
call stores the return address on stack before jumping to the given address
ret pops the return address from stack into PC reg. again.
(Other CPUs might do this differently.)
Additionally, the
push rbp
mov rbp, rsp
is interesting as push stores something on stack as well while rsp is the register with current stack top address and rbp its companion which is usually used for relative addressing of local variables.
So, a local variable (which is addressed relative to rbp – if not optimized) might have a fix offset to the return address on stack.
So, I added some code to the first sample to come in touch:
#include <stdio.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
Output:
&main(): 0x400613, &A(): 0x400553, &B(): 0x400542
call A():
in A()
0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00
0x7ffcdedc9760: (+40) 50 06 40 00 00 00 00 00
0x7ffcdedc9768: (+48) 30 48 4a f3 3e 7f 00 00
0x7ffcdedc9770: (+56) 00 00 00 00 00 00 00 00
Live Demo on coliru
This is what I read from this:
0x7ffcdedc9738: (+ 0) 00 de ad be ef 4a 11 00 # local var. buffer
0x7ffcdedc9740: (+ 8) 38 97 dc de fc 7f 00 00 # local var. pI (with address of buffer)
0x7ffcdedc9748: (+16) 60 97 dc de 14 00 00 00 # local var. i (4 bytes)
0x7ffcdedc9750: (+24) 60 97 dc de fc 7f 00 00 # pushed rbp
0x7ffcdedc9758: (+32) 49 06 40 00 00 00 00 00 # 0x400649 <- Aha!
0x400649 is an address which is slightly higher than the address of main() (0x400613). Considering, that there was some code in main() prior the call of A() this makes perfectly sense.
So, if I want to manipulate the return address this has to happen at pI + 32:
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
exit(0);
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
printf("Possible candidate for ret address: %p\n", *(void**)(pI + 32));
*(void**)(pI + 32) = (byte*)&B;
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
I.e. I "patch" the address of function B() as the return address into the stack.
Output:
&main(): 0x400696, &A(): 0x4005aa, &B(): 0x400592
call A():
in A()
0x7fffe0eb0858: (+ 0) 00 de ad be ef 4a 11 00
0x7fffe0eb0860: (+ 8) 58 08 eb e0 ff 7f 00 00
0x7fffe0eb0868: (+16) 80 08 eb e0 14 00 00 00
0x7fffe0eb0870: (+24) 80 08 eb e0 ff 7f 00 00
0x7fffe0eb0878: (+32) cc 06 40 00 00 00 00 00
0x7fffe0eb0880: (+40) e0 06 40 00 00 00 00 00
0x7fffe0eb0888: (+48) 30 c8 41 84 42 7f 00 00
0x7fffe0eb0890: (+56) 00 00 00 00 00 00 00 00
Possible candidate for ret address: 0x4006cc
in B()
Live Demo on coliru
Et voilà: in B().
Instead of assigning the address directly, the same could be achieved by storing a string with at least 40 chars into buffer (only 8 chars capacity):
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char byte;
void B()
{
puts("in B()");
exit(0);
}
void A()
{
puts("in A()");
char buffer[8] = { 0x00, 0xde, 0xad, 0xbe, 0xef, 0x4a, 0x11, 0x00 };
byte *pI = (byte*)buffer;
// dump some bytes from stack
for (int i = 0; i < 64; ++i) {
if (!(i % 8)) printf("%p: (+%2d)", pI + i, i);
printf(" %02x", pI[i]);
if (i % 8 == 7) putchar('\n');
}
// provoke buffer overflow vulnerability
printf("Input: "); fflush(stdout);
fgets(buffer, 40, stdin); // <- intentionally wrong use
// show result
putchar('\n');
}
int main()
{
printf("&main(): %p, &A(): %p, &B(): %p\n", (void*)&main, (void*)&A, (void*)&B);
puts("call A():");
A();
return 0;
}
Compiled and executed with:
$ gcc -std=c11 -O0 main.c
$ echo -e " \xa2\x06\x40\0\0\0\0\0" | ./a.out
To input the exact sequence of bytes by keyboard might be a bit difficult. Copy/paste might work. I used echo and redirection to keep things simple.
Output:
&main(): 0x4007ba, &A(): 0x4006ba, &B(): 0x4006a2
call A():
in A()
0x7ffd1700bac8: (+ 0) 00 de ad be ef 4a 11 00
0x7ffd1700bad0: (+ 8) c8 ba 00 17 fd 7f 00 00
0x7ffd1700bad8: (+16) f0 ba 00 17 14 00 00 00
0x7ffd1700bae0: (+24) f0 ba 00 17 fd 7f 00 00
0x7ffd1700bae8: (+32) f0 07 40 00 00 00 00 00
0x7ffd1700baf0: (+40) 00 08 40 00 00 00 00 00
0x7ffd1700baf8: (+48) 30 48 37 0f 5b 7f 00 00
0x7ffd1700bb00: (+56) 00 00 00 00 00 00 00 00
Input:
in B()
Live Demo on coliru
Please, note that the input of 32 spaces (to align the return address "\xa2\x06\x40\0\0\0\0\0" to the intended offset) "destroys" all the internals of A() which are stored in this range. This might have fatal consequences for the stability of the process but, eventually, it's intact enough to reach B() and report that to console.

i386-elf-gcc out put strange assembler command about "static a = 0"

i am write a mini os. And when i write this code to show time clock, its goes wrong
7 void timer_callback(pt_regs *regs)
8 {
9 static uint32_t tick = 0;
10 printf("Tick: %dtimes\n", tick);
11 tick++;
12 }
tick is initialise not with 0, but 1818389861. but if tick init with 0x01 or anything else zero, it's ok!!!
so i wirte a simple c file then objdump:
staic.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
extern void printf(char *, int);
int main(){
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl -0x4(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 51 push %ecx
e: 83 ec 04 sub $0x4,%esp
static int a = 1;
printf("%d\n", a);
11: a1 00 00 00 00 mov 0x0,%eax
16: 83 ec 08 sub $0x8,%esp
19: 50 push %eax
1a: 68 00 00 00 00 push $0x0
1f: e8 fc ff ff ff call 20 <main+0x20>
24: 83 c4 10 add $0x10,%esp
return 0;
27: b8 00 00 00 00 mov $0x0,%eax
}
2c: 8b 4d fc mov -0x4(%ebp),%ecx
2f: c9 leave
30: 8d 61 fc lea -0x4(%ecx),%esp
33: c3 ret
so strange, no memory used!!!
Update: let me say it clearly
the second static.c is an experiment, it was thought it show no memory used, but i was wrong, mov 0x0 %eab is. i confuse 0x0 and $0x0 /..\
my origin problem is why tick not succeed init with 0.(but can init with 1 or anyelsenumber).
i look up it again use gdb, ok, it do use memory like mov
eax,ds:0x106010,but the real strong thing is the memory x 0x106010 is not 0,but it should be, just as i said, if i let tick = 1 or anythingelse, memory do init as i want, that is the strange thing!
the tool: gdb ,objdump return different asm(different means,not formate),because, just learn os,not good at c, so i let it go,ignore it....
Memory is used, be sure of that; however, you won't find that memory in the .text section. Memory for static variables is allocated in either .bss (when zero-initialized; or, in case of C++, dynamically initialized) or .data (when non-zero initialized) section.
When dumping object files with objdump using the -d (disassembly) option, it is important to also use the -r (relocations) option. Without that, the disassembly you get is deceiving and makes little sense.
In your case, the instruction at addresses 11 and 1f must have relocations, at address 11, to the variable a and at address 1f, to the function printf. The instruction at address 11 loads the value from your variable a, without proper relocations it looks as if it loaded a value from address 0.
As to your original question, the value you get, 1818389861, or 0x6C626D65, is quite remarkable. I would bet that somewhere in your program you have a buffer overrun involving a string containing the subsequence embl.
As a side note, I would like to call your attention to the use of correct type specifications in printf calls. The type specification %d corresponds to the type int; on all modern mainstream architectures, int and int32_t are of the same size. However, that is not guaranteed to always be so. There are special type specifications for use with explicitly-sized types, for example, for an int32_t you use "PRId32":
uint32_t x;
printf("%"PRId32, x);

What numeric values defines in dissembled of C code?

I'm understanding the assembly and C code.
I have following C program , compiled to generate Object file only.
#include <stdio.h>
int main()
{
int i = 10;
int j = 22 + i;
return 0;
}
I executed following command
objdump -S myprogram.o
Output of above command is:
objdump -S testelf.o
testelf.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
#include <stdio.h>
int main()
{
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 10 sub $0x10,%esp
int i = 10;
6: c7 45 f8 0a 00 00 00 movl $0xa,-0x8(%ebp)
int j = 22 + i;
d: 8b 45 f8 mov -0x8(%ebp),%eax
10: 83 c0 16 add $0x16,%eax
13: 89 45 fc mov %eax,-0x4(%ebp)
return 0;
16: b8 00 00 00 00 mov $0x0,%eax
}
1b: c9 leave
1c: c3 ret
What is meant by number numeric before the mnemonic commands
i.e. "83 ec 10 " before "sub" command or
"c7 45 f8 0a 00 00 00" before "movl" command
I'm using following platform to compile this code:
$ lscpu
Architecture: i686
CPU op-mode(s): 32-bit
Byte Order: Little Endian
CPU(s): 1
On-line CPU(s) list: 0
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 1
Vendor ID: GenuineIntel
Those are x86 opcodes. A detailed reference, other than the ones listed in the comments above is available here.
For example the c7 45 f8 0a 00 00 00 before the movl $0xa,-0x8(%ebp) are hexadecimal values for the opcode bytes. They tell the CPU to move the immediate value of 10 decimal (as a 4-byte value) into the address located on the current stack 8-bytes above the stack frame base pointer. That is where the variable i from your C source code is located when your code is running. The top of the stack is at a lower memory address than the bottom of the stack, so moving a negative direction from the base is moving up the stack.
The c7 45 f8 opcodes mean to mov data and clear the arithmetic carry flag in the EFLAGS register. See the reference for more detail.
The remainder of the codes are an immediate value. Since you are using a little endian system, the least significant byte of a number is listed first, such that 10 decimal which is 0x0a in hexadecimal and has a 4-byte value of 0x0000000a is stored as 0a 00 00 00.

Smashing the stack not working

I have gone through the walkthrough about smashing the stack. Both the one http://insecure.org/stf/smashstack.html here and one I found on here Trying to smash the stack. I understand what is suppose to be happening, but I can't get it to work properly.
This is just like the other scenarios. I need to skip x=1 and print 0 as the value of x.
I compile with:
gcc file.c
The original code :
void function(){
char buffer[8];
}
void main(){
int x;
x = 0;
function();
x = 1;
printf("%d\n", x);
}
When I run
objdump -dS a.out
I get
0000000000400530 <function>:
400530: 55 push %rbp
400531: 48 89 e5 mov %rsp,%rbp
400534: 5d pop %rbp
400535: c3 retq
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 20 sub $0x20,%rsp
40053e: 89 7d ec mov %edi,-0x14(%rbp)
400541: 48 89 75 e0 mov %rsi,-0x20(%rbp)
400545: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
40054c: b8 00 00 00 00 mov $0x0,%eax
400551: e8 da ff ff ff callq 400530 <function>
400556: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
40055d: 8b 45 fc mov -0x4(%rbp),%eax
400560: 89 c6 mov %eax,%esi
400562: bf 10 06 40 00 mov $0x400610,%edi
400567: b8 00 00 00 00 mov $0x0,%eax
40056c: e8 9f fe ff ff callq 400410 <printf#plt>
400571: c9 leaveq
400572: c3 retq
400573: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40057a: 00 00 00
40057d: 0f 1f 00 nopl (%rax)
In the function I need to figure out how many bytes the return address is beyond the start of the buffer. I am not sure about this value. But since there are 6 bytes from the beginnig of the function to the return; would I add 7 bytes to the buffer?
Then I need to skip the instruction
x=1;
And since that instruction is 7 bytes long. Would I add 7 to return pointer?
Something like this?
void function(){
char buffer[8];
int *ret = buffer + 7;
(*ret) += 7;
}
void main(){
int x;
x = 0;
function();
x = 1;
printf("%d\n", x);
}
This throws the warning:
warning: initialization from incompatible pointer type [enabled by default]
int *ret = buffer1 + 5;
^
And the output is 1. What am I doing wrong? And can you explain how to do it right and why it is the correct way?
Thank you.
Try the function below, I wrote it for 32-bit compiler try using (-m32 gcc flag) or with a little effort you can make it work with your 64-bit compiler (Note that in your objdump listing you got 7 bytes offset between call to function and the next instruction so use 7 instead of 8.
void function(void)
{
unsigned long *x;
/* &x will more likely be at -4(ebp) */
/* Adding 1 (+4) gets us to stored ebp */
/* Adding 2 (+8) gets us to stored return address */
x = (unsigned long *)(&x + 2);
/* This is the tricky part */
/* TODO: On my 32-bit compiler gap between call to function
and the next instruction is 8 */
*x += 8;
}
We know that automatic variables are created on the stack - so taking the address of an automatic variable yields a pointer into the stack. When you call a void function, its return address is pushed onto the stack and the size of that address depends on your platform (4 or 8 bytes normally). So if you pass the address of an automatic variable to a function and then write over the memory before that address, you will damage the return address and smash the stack. Here is an example:
#include <stdlib.h>
#include <stdio.h>
static void f(int *p)
{
p[0] = 0x30303030;
p[1] = 0x31313131;
*(p - 1) = 0x35353535;
*(p - 2) = 0x36363636;
}
int main()
{
int a = 0x41424344;
int b = 0x45464748;
int c = 0x494a4b5c;
f(&b);
printf("%08x %08x %08x\n", a, b, c);
return 0;
}
I compiled this on linux with 'gcc -g' and ran under gdb and got this:
Program received signal SIGSEGV, Segmentation fault.
0x000000000040056a in f (p=0x7fffffffde74) at smash.c:10
10 }
(gdb) bt
#0 0x000000000040056a in f (p=0x7fffffffde74) at smash.c:10
#1 0x3636363600400594 in ?? ()
#2 0x3030303035353535 in ?? ()
#3 0x494a4b5c31313131 in ?? ()
#4 0x0000000000000000 in ?? ()
(gdb)
As you can see, the parent function addresses now contain some of my magic numbers. I ran this on 64 bit linux, so really I should have used 64 bit ints to fully overwrite the return address - as it is I left the lower word untouched.

Understanding disassembly - Seeing two main()'s

The dump of the following C program:
int main() {
int i,j;
for(i=0; i<2; i++) {
j++;
}
return 0;
}
is producing:
08048394 <main>:
int main() {
8048394: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048398: 83 e4 f0 and $0xfffffff0,%esp
804839b: ff 71 fc pushl -0x4(%ecx)
804839e: 55 push %ebp
804839f: 89 e5 mov %esp,%ebp
80483a1: 51 push %ecx
80483a2: 83 ec 10 sub $0x10,%esp
int i,j;
for(i=0; i<2; i++) {
80483a5: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp)
80483ac: eb 08 jmp 80483b6 <main+0x22>
j++;
80483ae: 83 45 f4 01 addl $0x1,-0xc(%ebp)
int main() {
int i,j;
for(i=0; i<2; i++) {
80483b2: 83 45 f8 01 addl $0x1,-0x8(%ebp)
80483b6: 83 7d f8 01 cmpl $0x1,-0x8(%ebp)
80483ba: 7e f2 jle 80483ae <main+0x1a>
j++;
}
return 0;
80483bc: b8 00 00 00 00 mov $0x0,%eax
}
No matter whether I put i<2 or i<10, I am seeing two main()'s with the same structure. Can someone tell me why this is happening?
You are not seeing two main()s. You are seeing a disassembler utterly confused out of its mind by a for loop. The actual assembly, if you read it all the way through, represents exactly one function, main(), and the logic path is identical to the C code.
In short: the C interleaved into the assembly is wrong.
The disassembler is dutifully interleaving the source code exactly as the compiler's output debug information says. On Linux, you can see this with objdump -W:
…
Line Number Statements:
Extended opcode 2: set Address to 0x80483e4
Copy
Special opcode 91: advance Address by 6 to 0x80483ea and Line by 2 to 3
Special opcode 132: advance Address by 9 to 0x80483f3 and Line by 1 to 4
Special opcode 60: advance Address by 4 to 0x80483f7 and Line by -1 to 3
Special opcode 148: advance Address by 10 to 0x8048401 and Line by 3 to 6
Special opcode 76: advance Address by 5 to 0x8048406 and Line by 1 to 7
Advance PC by 2 to 0x8048408
Extended opcode 1: End of Sequence
…
My compiler apparently differs a bit from yours, as the addresses are different, but you see how it works: the mapping between addresses in the output assembly and lines in the input source file is imprecise.

Resources