Android assigns different user IDs for each installed application as a privilege separation mechanism. This can be influenced by the certificate that is used to sign an application (https://source.android.com/security/overview/app-security.html#application-signing) in conjunction with the
android:sharedUserId attribute in the application's AndroidManifest.xml file:
When an application (APK file) is installed onto an Android device, the Package Manager verifies that the APK has been properly signed with the certificate included in that APK. If the certificate (or, more accurately, the public key in the certificate) matches the key used to sign any other APK on the device, the new APK has the option to specify in the manifest that it will share a UID with the other similarly-signed APKs.
Android applications launched by end users are all spawned from the Zygote process (this is to help speed application launch, since the Zygote can fork and load the application code and not have to reload a bunch of system libraries). At the time of writing this, the Zygote service configuration looks like this:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
The syntax of this file is outlined here: https://android.googlesource.com/platform/system/core/+/master/init/readme.txt, which has this to say about the privilege levels assigned to services by default (so you can see that by default, Zygote runs as root):
Change to username before exec'ing this service.
Currently defaults to root. (??? probably should default to nobody)
Currently, if your process requires linux capabilities then you cannot use
this command. You must instead request the capabilities in-process while
still root, and then drop to your desired uid.
Zygote uses a JNI method named forkAndSpecialize (search for "
com_android_internal_os_Zygote.cpp") to handle the platform specific task of calling setuid() to the UID that the Package Manager has assigned based on the manifest and installed applications. Failing to properly exit if that setuid call failed has been the source of Android privilege escalation vulnerabilities in the past (read up on the RageAgainstTheCage exploit if you are interested, although that was not Zygote related).
This is the main reason that you need a separate privileged/setuid binary to launch applications using root level privileges. If you built your own custom system image, you could conceivably modify the PackageManager / Zygote to retain the root privileges, but doing so basically breaks the Android security model.