What happens "under the hood" is that each ASP.NET page is compiled into a .NET class, just like any other. Controls (ASCX) are compiled the same way. There is a compiler for ASP.NET markup just like there is a compiler for c# and VB.NET - you can think of the ASP.NET markup as "yet another" language which compiles to native MSIL.
The native ASP.NET markup is handled by the PageParser. You may notice this class has one interesting and very useful method - GetCompiledPageInstance. As it implies, it compiles the page to a native .NET class. This can be overridden (e.g., you could come up with your own markup and write your own parser/compiler). Spark is a popular alternative to ASP.NET markup.
Each class ultimately inherits from Page or Control. Both of these classes have Render() methods which, at the end of the class's executing its custom functionality you have implemented, write HTML to an output stream.
The big difference is that the compilation often happens at a much different time than the c# or VB.NET code. This is not really a technical requirement so much as it is a feature that permits decoupling the presentation .NET language from the functional .NET language. ASPX and ASCX pages are compiled by the ASP.NET runtime when they are requested in the context of a web server. The compiled assemblies are then held in memory until a) the web application shuts down or b) a filesystem change is detected in one of the ASPX files, triggering a recompile.
It is possible to compile your ASPX pages alongside your c#/VB.NET/whatever, so you can deploy the entire application as a single assembly. This has two advantages - one, the obvious ease of deploying a single DLL. Second, it eliminates the LOOOONG wait time that so often accompanies a "first hit" to an ASP.NET web application. Since all the pages are precompiled, the assembly simply needs to be loaded into memory upon first request, instead of compiling each file.