s is not initialized, so it holds some garbage address (probably an invalid one).
When you do *s = "Hello World!";
you are writing "Hello World!"
(which is a pointer value) to some garbage address (probably an invalid one).
Let's say it doesn't crash though - then puts
will read the bytes from that same garbage address (i.e. it will read the address of the string, not the string) and display them on the screen.
After running the incorrect code the memory might contain these values for example:
Address Value (4 bytes at a time)
...
0x12345678 0x65401234 <- some important thing you just overwrote that is liable to make your program crash,
now it holds the address of the string literal
...
0x4000000C 0x12345678 <- variable 's' in main
0x40000010 0x12345678 <- variable 's' in function, copy of variable 's' in main
...
0x65401234 'H', 'e', 'l', 'l' <- where the compiler decided to put the string literal
0x65401238 'o', ' ', 'W', 'o'
0x6540123C 'r', 'l', 'd', '!'
0x65401240 0
When you call puts(s);
you would be calling puts(0x12345678);
and it would print the bytes 0x65401234
(but it wouldn't print "0x65401234", it'd try to print the letters corresponding to those)
If you do it right, you end up with:
Address Value (4 bytes at a time)
...
0x4000000C 0x65401234 <- variable 's' in main
0x40000010 0x4000000C <- variable 's' in function, has address of variable 's' in main
...
0x65401234 'H', 'e', 'l', 'l' <- where the compiler decided to put the string literal
0x65401238 'o', ' ', 'W', 'o'
0x6540123C 'r', 'l', 'd', '!'
0x65401240 0
Then puts(s)
is puts(0x65401234)
which prints the string.