Okay, I figured this out now, after some experimenting of my own.
Since passing an array that starts with NULL (0) to execv results in the program printing out LC_MEASUREMENT=de_DE.UTF-8, I realized that it must means that argv[3] is referring to an element in the process environment (LC_MEASUREMENT is one of the environment variables used to configure locale settings in Linux).
Solution with execv
Since execv will copy the current environment to the new program, we just have to modify the environment and put the string "Hello World!" in the correct spot before calling execv. It turns out that the string which gets printed is the one pointed to by index 2 of the environment.
To access the current environment, we need to declare the environ variable outside of main:
external char **environ;
int main()
{
...
And then do this before calling execv:
char *argv[] = { NULL };
environ[2] = "Hello world!";
execv("filepath", argv);
Simpler solution with execve
Instead of declaring external char **environ and modifying the current environment before calling execv, we can use the execve function, which lets us pass in a new array of strings to use as the environment for the program:
char *argv[] = { NULL };
char *envp[] = { "foo", "bar", "Hello World!", NULL };
execve("filepath", argv, envp);
As the above code snippet shows, "Hello World!" needs to be at index 2 of the environment, regardless of which method we use.
How it works
The reason this works is that there's a standard for how command-line arguments and the environment are laid out in process memory when a program is executed: The environment array of char pointers is placed right after the command-line arguments array.
Since these arrays are terminated with a NULL entry, it will look like this:
+---------+---------------------------------------+
| Args | Environment |
+---------+---------+---------+---------+---------+
| NULL | envp[0] | envp[1] | envp[2] | NULL |
+---------+---------+---------+---------+---------+
^ ^ ^
| | |
argv[0] argv[1] ... argv[3]
I hope the ASCII art helps you understand why argv[3] means the same thing as environ[2] when we execute the program like this.