Arrays pointers on 32bit and 64bit systems - c

The following code prints different results on 32bit and 64bit systems:
#include <stdio.h>
void swapArray(int **a, int **b)
{
int *temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int a[2] = {1, 3};
int b[2] = {2, 4};
swapArray(&a, &b);
printf("%d\n", a[0]);
printf("%d\n", a[1]);
return 0;
}
After compiling it in 32bit system, the output is:
2
3
On 64bit the output is:
2
4
As I understand, the function swapArray just swaps the pointers to the first elements in a and b. So after calling swapArray, a should point to 2 and b should point to 1.
For this reason a[0] should yield 2, and a[1] should reference the next byte in memory after the location of 2, which contains 4.
Can anyone please explain?
Edit:
Thanks to the comments and answers, I now notice that &a and &b are of type int (*)[] and not int **. This obviously makes the code incorrect (and indeed I get a compiler warning). It is intriguing, though, why the compiler (gcc) just gives a warning and not an error.
I am still left with the question what causes different results on different systems, but since the code is incorrect, it is less relevant.
Edit 2:
As for the different results on different systems, I suggest reading AndreyT's comment.

swapArray(&a, &b);
&a and &b are not of type int ** but of type int (*)[2]. BTW your compiler is kind enough to accept your program but a compiler has the right to refuse to translate it.

Before answering your question lets see what happens under the hood during a pointer operation. I'm using a very simple code to demonstrate this :
#include <stdio.h>
int main() {
int *p;
int **p2;
int x = 3;
p = &x;
p2 = &p;
return 0;
}
Now look at the disassembly :
(gdb) disassemble
Dump of assembler code for function main:
0x0000000000400474 <+0>: push rbp
0x0000000000400475 <+1>: mov rbp,rsp
0x0000000000400478 <+4>: mov DWORD PTR [rbp-0x14],0x3
0x000000000040047f <+11>: lea rax,[rbp-0x14]
0x0000000000400483 <+15>: mov QWORD PTR [rbp-0x10],rax
0x0000000000400487 <+19>: lea rax,[rbp-0x10]
0x000000000040048b <+23>: mov QWORD PTR [rbp-0x8],rax
=> 0x000000000040048f <+27>: mov eax,0x0
0x0000000000400494 <+32>: leave
0x0000000000400495 <+33>: ret
The disassembly is pretty self evident. But a few note need to be added here,
My function's stack frame starts from here:
0x0000000000400474 <+0>: push rbp
0x0000000000400475 <+1>: mov rbp,rsp
So lets what they have for now
(gdb) info registers $rbp
rbp 0x7fffffffe110 0x7fffffffe110
here we are putting value 3 in [rbp - 0x14]'s address. lets see the memory map
(gdb) x/1xw $rbp - 0x14
0x7fffffffe0fc: 0x00000003
Its important to notice the DWORD datatype is used, which is a 32 bits wide. So on the side note, integer literals like 3 is treated treated as 4 bytes unit.
Next instruction uses lea to load the effective address of the value just saved in earlier instruction.
0x000000000040047f <+11>: lea rax,[rbp-0x14]
It means that now $rax will have the value 0x7fffffffe0fc.
(gdb) p/x $rax
$4 = 0x7fffffffe0fc
Next we will save this address into memory using
0x0000000000400483 <+15>: mov QWORD PTR [rbp-0x10],rax
Important thing to note that a QWORD which is used here. Because 64bit systems have 8 byte native pointer size. 0x14 - 0x10 = 4 bytes were used in earlier mov instruction.
Next we have :
0x0000000000400487 <+19>: lea rax,[rbp-0x10]
0x000000000040048b <+23>: mov QWORD PTR [rbp-0x8],rax
This is again for the second indirection. always all the value related to addresses are QWORD. This is important thing to take a note of this.
Now lets come to your code.
Before calling to swaparray you have :
=> 0x00000000004004fe <+8>: mov DWORD PTR [rbp-0x10],0x1
0x0000000000400505 <+15>: mov DWORD PTR [rbp-0xc],0x3
0x000000000040050c <+22>: mov DWORD PTR [rbp-0x20],0x2
0x0000000000400513 <+29>: mov DWORD PTR [rbp-0x1c],0x4
0x000000000040051a <+36>: lea rdx,[rbp-0x20]
0x000000000040051e <+40>: lea rax,[rbp-0x10]
0x0000000000400522 <+44>: mov rsi,rdx
0x0000000000400525 <+47>: mov rdi,rax
This is very trivial. Your array is initialized and the effect of & operator is visible when the effective address of the start of array is loaded into $rdi and $rsi.
Now lets see what its doing inside swaparray().
The start of your array is saved into $rdi and $rsi. So lets see their contents
(gdb) p/x $rdi
$2 = 0x7fffffffe100
(gdb) p/x $rsi
$3 = 0x7fffffffe0f0
0x00000000004004c8 <+4>: mov QWORD PTR [rbp-0x18],rdi
0x00000000004004cc <+8>: mov QWORD PTR [rbp-0x20],rsi
Now the first statement int *temp = *a is performed by following instructions.
0x00000000004004d0 <+12>: mov rax,QWORD PTR [rbp-0x18]
0x00000000004004d4 <+16>: mov rax,QWORD PTR [rax]
0x00000000004004d7 <+19>: mov QWORD PTR [rbp-0x8],rax
Now comes the defining moment, what's happening with your *a?
It loads into $rax the value stored in [rbp - 0x18]. where the value $rdi was saved. which in turn holds the address of the first element of the first array.
performs another indirection by using the address stored into $rax to fetch a QWARD and loads it into $rax. So what it will return? it will return a QWARD from 0x7fffffffe100. Which will in effect form a 8 byte quantity from two four byte quantity saved there. To elaborate,
The memory there is like below.
(gdb) x/2xw $rdi
0x7fffffffe100: 0x00000001 0x00000003
Now if you fetch a QWORD
(gdb) x/1xg $rdi
0x7fffffffe100: 0x0000000300000001
So already you are actually screwed. Because you are fetching with incorrect boundary.
The rest of the codes can be explained in similar manner.
Now why its different in 32 bit platform? because in 32 bit platform the native pointer width is 4 bytes. So the thing here will be different there. The main problem with your semantically incorrect code originates from the difference in integer type width and native pointer types. If you have both the same, you may still work around your code.
But you should never write code which assumes the size of native types. That's why standards are for. that's why your compiler is giving you warning.
From language point of view its a type mismatch which is already pointed out in the earlier answers so i'm not going into that.

You can't swap arrays using the pointer trick (they are not pointers!). You would either have to create pointers to those arrays and use the pointers or dynamically allocate the arrays using malloc etc.
The results I get on a 64-bit system are different than yours for example, I get:
2
3
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.8, not stripped
And with clang on my mac I get an error:
test.cpp: In function ‘int main()’:
test.cpp:13: error: cannot convert ‘int (*)[2]’ to ‘int**’ for argument ‘1’ to ‘void swapArray(int**, int**)’
I assume that this is undefined behavior and you are trying to interpret what is probably junk output.

Related

Why this program need more than 45 input to occur buffer overflow(segmentaion fault)?

Why this program needs more than 45 input to occur buffer overflow(segmentaion fault)?
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
char whatever[20];
strcpy(whatever, argv[1]);
return 0;
}
I mean it should be more than 24 char input.by the way there is no grsecurity enabled in my system.and i'm using ubuntu 7.04 32bit on virtual box.
Ok, what's interesting here is the disassembly of main:
push %ebp
mov %esp,%ebp
sub $0x38,%esp
and $0xfffffff0,%esp
mov $0x0,%eax
sub %eax,%esp
mov 0xc(%ebp),%eax
add $0x4,%eax
mov (%eax),%eax
mov %eax,0x4(%esp)
lea 0xffffffd8(%ebp),%eax
mov %eax,(%esp)
call 80482a0 <strcpy#plt>
mov $0x0,%eax
leave
ret
Before entering main, the stack pointer esp points to the return address pushed by call. Let's call that &ret.
The first opcode in the function pushes the base pointer of the previous frame, and then sets the current base pointer to the stack pointer. So ebp = &ret - 4.
When setting up the call to strcpy, the value right at esp is the first parameter. Here:
mov %eax,(%esp)
call 80482a0 <strcpy#plt>
So the value in eax is the first parameter. If we look at the previous instruction, we can see what that value is:
lea 0xffffffd8(%ebp),%eax
Ok, this notation basically means: eax = ebp + 0xffffffd8, which is equivalent to eax = ebp - 40 (see Two's Complement). Basically, you flip all the bits (and get 0x27=39), stick a minus sign (-39), and subtract 1 (-40).
And in relation to the frame's return address: eax = &ret - 44
So it would take at least 45 bytes to overrun the return address.
But you say 47. This is interesting, and it might have to do with the specific input you supplied.
You see, x86 is a little-endian little endian machine, which means that in memory, integers are stored LSB-first. So, when overwriting the stored return address, you first overwrite it's LSB.
If your input happens to be in the vicinity of the LSB, you might cause a faulty termination, but not a segmentation fault, as you will cause a branch to a legitimate address.
If you'll share your input, it might help shed some light on those two missing bytes :)

Using the Ollydbg,anyone tell me what the address of the variable "a" is?

My very simple tested program
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a = 12345;
printf("%d\n", a);
system("PAUSE");
return 0;
}
After compiled and connected,the EXE file is created.Then I open the EXE file in the Ollydbg:
The picture shows the main() function.But I can't find out what the address of the variable a is. When passing the params to the printf() function,it push 3039 into the stack, then it means the value of the variable a is 3039? No,the value is 12345. So it means the address of the variable a is 00003039? Anyone
Address of the a variable is [ebp-8]. You are seeing 0x3039 assignment, because decimal 12345 is hexadecimal 0x3039. If you change your code to use hex value: int a = 0x12345, results would be more clear:
Numeric constants are usually compiled directly into the code.
so you want to know the address of the variable a.
Extremely simple:
insert this statement into your program.
printf( "address of variable 'a' is: %p\n", &a );
How are we to know the address?
We are not sitting at your computer
and it will be different on most every computer.
Use the suggested call to printf() to learn the 'address'
HOWEVER, due to paging, address translation, virtual addressing, etc. That address is NOT the actual physical address within your computers' memory space.
local variables like in this case, a, are stored in the stack, so they can be discarded when a function execution is finished, so basically a is simply located at the memory address of the local stack frame.
int a = 12345; // MOV DWORD PTR SS:[EBP-8], 3039
printf("%d\n", a);
in this case, a is located in [EBP-8], if you inspect where it is pointing, you can see the value 3039, stored in there of course after the assignment, 3039 is a hex number, which of course 12345 in base 10.
To get better understanding of this, let's modify the program a little bit and debug it in GDB.
C:\Codes>gdb test -q
Reading symbols from C:\Codes\test.exe...done.
(gdb) set disassembly-flavor intel
(gdb) list
1 #include<stdio.h>
2
3 int main()
4 {
5 int a = 12345;
6 int b = 0x12345;
7 printf("Variable a %d (decimal) or 0x%x (hex), located at %p or 0x%x\n", a,a,&a,&a);
8 printf("Variable b %d (decimal) or 0x%x (hex), located at %p or 0x%x\n", b,b,&b,&b);
9 return 0;
10 }
(gdb)
Standard Output
C:\Codes>test
Variable a 12345 (decimal) or 0x3039 (hex), located at 0022FF4C or 0x22ff4c
Variable b 74565 (decimal) or 0x12345 (hex), located at 0022FF48 or 0x22ff48
As you can see, the virtual memory addresses of variable a and b is actually located at 0x22ff4c and 0x22ff48 respectively.
Let's take a look at this program in GDB.
(gdb) break 7
Breakpoint 1 at 0x40135e: file test.c, line 7.
(gdb) run
Starting program: C:\Codes/test.exe
[New Thread 3680.0xed8]
Breakpoint 1, main () at test.c:7
7 printf("Variable a %d (decimal) or 0x%x (hex), located at %p or 0x%x\n", a,a,&a,&a);
(gdb) disassemble
Dump of assembler code for function main:
0x00401340 <+0>: push ebp
0x00401341 <+1>: mov ebp,esp
0x00401343 <+3>: and esp,0xfffffff0
0x00401346 <+6>: sub esp,0x30
0x00401349 <+9>: call 0x401970 <__main>
0x0040134e <+14>: mov DWORD PTR [esp+0x2c],0x3039
0x00401356 <+22>: mov DWORD PTR [esp+0x28],0x12345
=> 0x0040135e <+30>: mov edx,DWORD PTR [esp+0x2c]
0x00401362 <+34>: mov eax,DWORD PTR [esp+0x2c]
0x00401366 <+38>: lea ecx,[esp+0x2c]
0x0040136a <+42>: mov DWORD PTR [esp+0x10],ecx
0x0040136e <+46>: lea ecx,[esp+0x2c]
0x00401372 <+50>: mov DWORD PTR [esp+0xc],ecx
0x00401376 <+54>: mov DWORD PTR [esp+0x8],edx
0x0040137a <+58>: mov DWORD PTR [esp+0x4],eax
0x0040137e <+62>: mov DWORD PTR [esp],0x403024
0x00401385 <+69>: call 0x401be0 <printf>
0x0040138a <+74>: mov edx,DWORD PTR [esp+0x28]
0x0040138e <+78>: mov eax,DWORD PTR [esp+0x28]
0x00401392 <+82>: lea ecx,[esp+0x28]
0x00401396 <+86>: mov DWORD PTR [esp+0x10],ecx
0x0040139a <+90>: lea ecx,[esp+0x28]
0x0040139e <+94>: mov DWORD PTR [esp+0xc],ecx
0x004013a2 <+98>: mov DWORD PTR [esp+0x8],edx
0x004013a6 <+102>: mov DWORD PTR [esp+0x4],eax
0x004013aa <+106>: mov DWORD PTR [esp],0x403064
0x004013b1 <+113>: call 0x401be0 <printf>
0x004013b6 <+118>: mov eax,0x0
0x004013bb <+123>: leave
0x004013bc <+124>: ret
End of assembler dump.
(gdb)
And focus on this line
0x0040134e <+14>: mov DWORD PTR [esp+0x2c],0x3039
0x00401356 <+22>: mov DWORD PTR [esp+0x28],0x12345
As you can see from the previous output, the virtual memory address of variables a and b is actually located at [esp+0x2c] or 0x22ff4c and [esp+0x28] or 0x22ff48 respectively.
while
0x3039 & 0x12345 are the value of variables a and b in hexadecimal.
To verify the memory address of these variables in GDB, use print command as follows:
(gdb) print &a
$1 = (int *) 0x22ff4c
(gdb) print &b
$2 = (int *) 0x22ff48
Also, you might wonder where the address of 0x22ff4c or 0x22ff48 come from.
To understand this, let's check the value of current ESP register
(gdb) info registers esp
esp 0x22ff20 0x22ff20
Then, replace the actual ESP value
[esp+0x2c] = [0x22ff20 + 0x2c] = 0x22ff4c
[esp+0x28] = [0x22ff20 + 0x28] = 0x22ff48

How is this string in an array represented in assembly when a C program is compiled using the gcc -S option?

This is a C program, which has been compiled to assembly using gcc -S. How is string "Hello, world" represented in this program?
This is the C-code:
1. #include <stdio.h>
2.
3. int main(void) {
4.
5. char st[] = "Hello, wolrd";
6. printf("%s\n", st);
7.
8. return 0;
9. }
Heres the assembly code:
1. .intel_syntax noprefix
2. .text
3. .globl main
4.
5. main:
6. push rbp
7. mov rbp, rsp
8. sub rsp, 32
9. mov rax, QWORD PTR fs:40
10 mov QWORD PTR [rbp-8], rax
11. xor eax, eax
12. movabs rax, 8583909746840200520
15. mov QWORD PTR [rbp-32], rax
14. mov DWORD PTR [rbp-24], 1684828783
15. mov BYTE PTR [rbp-20], 0
16. lea rax, [rbp-32]
17. mov rdi, rax
18. call puts
19. mov eax, 0
20. mov rdx, QWORD PTR [rbp-8]
21. xor rdx, QWORD PTR fs:40
22 je .L3
22. call __stack_chk_fail
23. .L3:
24. leave
25. ret
You are using a local buffer in function main, initialized from a string literal. The compiler compiles this initialization as setting the 16 bytes at [rbp-32] with 3 mov instructions. The first one via rax, the second immediate as the value is 32 bits, the third for a single byte.
8583909746840200520 in decimal is 0x77202c6f6c6c6548 in hex, corresponding to the bytes "Hello, W" in little endian order, 1684828783 is 0x646c726f, the bytes "orld". The third mov sets the final '\0' byte. Hence the buffer contains "Hello, World".
This string is then passed to puts for output to stdout.
Note that gcc optimized the call printf("%s\n", "Hello, World"); to puts("Hello, World");! By the way, clang performs the same optimization.
Interesting.
If you'd written const char *str="...", gcc would have passed puts a pointer to the string sitting there in the .rodata section, like in this godbolt link. (Well-spotted by chqrlie that gcc is optimizing printf to puts).
Your code forces the compiler to make a writeable copy of the string literal, by assigning it to a non-const char[]. (Actually, even with const char str[], gcc still generates it on the fly from mov-immediates. clang-3.7 spots the chance to optimize, though.)
Interestingly, it encodes it into immediate data, rather than copying into the buffer. If the array had been global, it would have just been sitting there in the regular .data section, not .rodata.
Also, in general avoid using main() to see compiler optimization. gcc on purpose marks it as "cold", and optimizes it less. This is an advantage for real programs that do their real work in other functions. No difference in this case, renaming main. But usually if you're looking at how gcc optimizes something, it's best to write a function that takes some args, and use those. Then you don't have to worry about gcc seeing that the inputs or loop-bounds are compile-time constants, either.

Smashing the stack example3 ala Aleph One

I've reproduced Example 3 from Smashing the Stack for Fun and Profit on Linux x86_64. However I'm having trouble understanding what is the correct number of bytes that should be incremented to the return address in order to skip past the instruction:
0x0000000000400595 <+35>: movl $0x1,-0x4(%rbp)
which is where I think the x = 1 instruction is. I've written the following:
#include <stdio.h>
void fn(int a, int b, int c) {
char buf1[5];
char buf2[10];
int *ret;
ret = buf1 + 24;
(*ret) += 7;
}
int main() {
int x;
x = 0;
fn(1, 2, 3);
x = 1;
printf("%d\n", x);
}
and disassembled it in gdb. I have disabled address randomization and compiled the program with the -fno-stack-protector option.
Question 1
I can see from the disassembler output below that I want to skip past the instruction at address 0x0000000000400595: both the return address from callq <fn> and the address of the movl instruction. Therefore, if the return address is 0x0000000000400595, and the next instruction is 0x000000000040059c, I should add 7 bytes to the return address?
0x0000000000400572 <+0>: push %rbp
0x0000000000400573 <+1>: mov %rsp,%rbp
0x0000000000400576 <+4>: sub $0x10,%rsp
0x000000000040057a <+8>: movl $0x0,-0x4(%rbp)
0x0000000000400581 <+15>: mov $0x3,%edx
0x0000000000400586 <+20>: mov $0x2,%esi
0x000000000040058b <+25>: mov $0x1,%edi
0x0000000000400590 <+30>: callq 0x40052d <fn>
0x0000000000400595 <+35>: movl $0x1,-0x4(%rbp)
0x000000000040059c <+42>: mov -0x4(%rbp),%eax
0x000000000040059f <+45>: mov %eax,%esi
0x00000000004005a1 <+47>: mov $0x40064a,%edi
0x00000000004005a6 <+52>: mov $0x0,%eax
0x00000000004005ab <+57>: callq 0x400410 <printf#plt>
0x00000000004005b0 <+62>: leaveq
0x00000000004005b1 <+63>: retq
Question 2
I notice that I can add 5 bytes to the return address in place of 7 and achieve the same result. When I do so, am I not jumping into the middle of the instruction 0x0000000000400595 <+35>: movl $0x1,-0x4(%rbp)? In which case, why does this not crash the program, like when I add 6 bytes to the return address in place of 5 bytes or 7 bytes.
Question 3
Just before buffer1[] on the stack is SFP, and before it, the return address.
That is 4 bytes pass the end of buffer1[]. But remember that buffer1[] is
really 2 word so its 8 bytes long. So the return address is 12 bytes from
the start of buffer1[].
In the example by Aleph 1, he/she calculates the offset of the return address as 12 bytes from the start of buffer1[]. Since I am on x86_64, and not x86_32, I need to recalculate the offset to the return address. When on x86_64, is it the case that buffer1[] is still 2 words, which is 16 bytes; and the SFP and return address are 8 bytes each (as we're on 64 bit) and therefore the return address is at: buf1 + (8 * 2) + 8 which is equivalent to buf1 + 24?
The first, and very important, thing to note: all numbers and offsets are very compiler-dependent. Different compilers, and even the same compiler with different settings, can produce drastically different assemblies. For example, many compilers can (and will) remove buf2 because it's not used. They can also remove x = 0 as its effect is not used and later overwritten. They can also remove x = 1 and replace all occurences of x with a constant 1, etc, etc.
That said, you absolutely need to make numbers for a specific assembly you're getting on your specific compiler and its settings.
Question 1
Since you provided the assembly for main(), I can confirm that you need to add 7 bytes to the return address, which would normally be 0x0000000000400595, to skip over x=1 and go to 0x000000000040059c which loads x into register for later use. 0x000000000040059c - 0x0000000000400595 = 7.
Question 2
Adding just 5 bytes instead of 7 will indeed jump into middle of instruction. However, this 2-byte tail of instruction happen (by pure chance) to be another valid instruction code. This is why it doesn't crash.
Question 3
This is again very compiler and settings dependent. Pretty much everything can happen there. Since you didn't provide disassembly, I can only make guesses. The guess would be the following: buf and buf2 are rounded up to the next stack unit boundary (8 bytes on x64). buf becomes 8 bytes, and buf2 becomes 16 bytes. Frame pointers are not saved to stack on x64, so no "SFP". That's 24 bytes total.

Multi dimensional Arrays

#include <stdio.h>
int multi[2][3] = {{17, 23, 19}, {72, 34, 44}};
int main()
{
printf("%p\n", multi); //line 1
printf("%p\n", *multi); //line 2
if(*multi == multi)
puts("They are equal!");
return 0;
}
How line 1 and 2 is different?
I'm getting output :
They are equal
Also can somebody refer a good tutorial on pointers and its use with multidimensional arrays .. .
The value is the same but the type is different.
multi is of type int [2][3] and when evaluated it is converted to the type int (*)[3]
*multi is of type int [3] and when evaluated it is of type int *.
Actually:
*multi == multi
is accepted by your compiler but the expression is not valid in C because the two operands of the == operators are of different types. To perform the comparison you would need to cast one of the two operands.
The question to you your answer is given by gcc when you compile your code:
ml.c:10: warning: comparison of distinct pointer types lacks a cast
multi is a type of 2 dimensional int array. that is int[][]
Where *multi is a type of 1 dimensional int array. That is int[]
That's why they are not the same object. One has to be cast to be eligible for comparison. Lets see how this wrong code works under the hood.
Surprisingly there's no cmp instruction at all!(compiled with -g -O0). Actually you don't need a cmp here. Because multi will be decayed to a pointer to &multi[0][0]. and *multi will be decayed to &multi[0]. So from the memory's point of view, they are the same, and c compiler happily optimizes them (even with -O0 :)).
(gdb) disassemble
Dump of assembler code for function main:
0x0000000000400504 <+0>: push rbp
0x0000000000400505 <+1>: mov rbp,rsp
=> 0x0000000000400508 <+4>: mov eax,0x400648
0x000000000040050d <+9>: mov esi,0x600900
0x0000000000400512 <+14>: mov rdi,rax
0x0000000000400515 <+17>: mov eax,0x0
0x000000000040051a <+22>: call 0x4003f0 <printf#plt>
0x000000000040051f <+27>: mov edx,0x600900
0x0000000000400524 <+32>: mov eax,0x400648
0x0000000000400529 <+37>: mov rsi,rdx
0x000000000040052c <+40>: mov rdi,rax
0x000000000040052f <+43>: mov eax,0x0
0x0000000000400534 <+48>: call 0x4003f0 <printf#plt>
0x0000000000400539 <+53>: mov edi,0x40064c
0x000000000040053e <+58>: call 0x400400 <puts#plt>
0x0000000000400543 <+63>: mov eax,0x0
0x0000000000400548 <+68>: leave
0x0000000000400549 <+69>: ret
only thing its doing before calling puts() is moving the address of the string which it should print into argument register.
gdb) x/10cb 0x40064c
0x40064c <__dso_handle+12>: 84 'T' 104 'h' 101 'e' 121 'y' 32 ' ' 97 'a' 114 'r' 101 'e'
0x400654 <__dso_handle+20>: 32 ' ' 101 'e'
There you go, you are confusing the compiler enough :) that it stripped away the cmp may with a always true optimization. :)
Expert C programming has a chapter named (surprise! surprise!)
Chapter 4. The Shocking Truth: C Arrays and Pointers Are NOT the Same!
Highly recommended.
When you compile with -Wall, the compiler will warn you about the line (cause: comparison of distinct pointer types):
if(*multi == multi)
multi is array and in C his address is the address of his first element aka multi[0].
*multi is pointer to the first element of the array aka multi[0].
You are comparing two addresses that contain the same data: ({17, 23, 19}), which explain why you get this output.
Hope this help.
Regards.
In brief in C a matrix is stored as a series of consecutive arrays wich are the rows of the matrix :
m has type pointer to array of 3 ints
and is the address of the first array / row of the matrix
m* has type pointer to int
and is the address of the first
element of the first row
the same apply to m+1 wich is the address of the second array
and for *( m + 1 ) wich is the address of the first element of the second array/row.
hope this helps.

Resources