Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

300
Views
¿Por qué necesita volver a compilar C/C++ para cada sistema operativo?

Esta es más una pregunta teórica que otra cosa. Soy un estudiante de ciencias de la computación con un gran interés en la programación de bajo nivel. Me encanta descubrir cómo funcionan las cosas bajo el capó. Mi especialización es el diseño de compiladores.

De todos modos, mientras trabajo en mi primer compilador, se me ocurren cosas que son un poco confusas.

Cuando escribe un programa en C/C++, lo tradicional que la gente sabe es que un compilador convierte mágicamente su código C/C++ en código nativo para esa máquina.

Pero algo no cuadra aquí. Si compilo mi programa C/C++ dirigido a la arquitectura x86, parecería que el mismo programa debería ejecutarse en cualquier computadora con la misma arquitectura. Pero eso no sucede. Debe volver a compilar su código para OS X, Linux o Windows (y una vez más para 32 bits frente a 64 bits)

Me pregunto por qué este es el caso. ¿No apuntamos a la arquitectura/conjunto de instrucciones de la CPU al compilar un programa C/C++? Y un sistema operativo Mac y un sistema operativo Windows pueden ejecutarse en la misma arquitectura exacta.

(Sé que Java y similares apuntan a una VM o CLR, por lo que no cuentan)

Si tomé la mejor respuesta a esto, diría que C/C++ debe compilar las instrucciones específicas del sistema operativo. Pero cada fuente que leo dice que el compilador apunta a la máquina. Así que estoy muy confundido.

over 4 years ago · Santiago Trujillo
7 answers
Answer question

0

Si compilo mi programa C/C++ dirigido a la arquitectura x86, parecería que el mismo programa debería ejecutarse en cualquier computadora con la misma arquitectura.

Es muy cierto, pero hay algunos matices.

Consideremos varios casos de programas que son, desde el punto de vista del lenguaje C, independientes del sistema operativo.


  1. Suponga que todo lo que hace su programa, desde el principio, es probar la CPU haciendo muchos cálculos sin E/S.

El código de máquina podría ser exactamente el mismo para todos los sistemas operativos (siempre que todos se ejecuten en el mismo modo de CPU, por ejemplo, modo protegido x86 de 32 bits). Incluso podría escribirlo en lenguaje ensamblador directamente, no sería necesario adaptarlo para cada sistema operativo.

Pero cada sistema operativo quiere encabezados diferentes para los archivos binarios que contienen este código. Por ejemplo, Windows quiere el formato PE , Linux necesita ELF , macOS usa el formato Mach-O . Para su programa simple, puede preparar el código de la máquina como un archivo separado y un montón de encabezados para el formato ejecutable de cada sistema operativo. Entonces, todo lo que necesita para "recompilar" sería concatenar el encabezado y el código de la máquina y, posiblemente, agregar un "pie de página" de alineación.

Entonces, supongamos que compiló su código C en código de máquina, que se ve así:

 offset: instruction disassembly 00: f7 e0 mul eax 02: eb fc jmp short 00

Este es el código simple de prueba de estrés, haciendo repetidamente multiplicaciones del registro eax por sí mismo.

Ahora desea que se ejecute en Linux de 32 bits y Windows de 32 bits. Necesitará dos encabezados, aquí hay ejemplos (volcado hexadecimal):

  • Para Linux:
 000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 >.ELF............< 000010 02 00 03 00 01 00 00 00 54 80 04 08 34 00 00 00 >........T...4...< 000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 28 00 >........4. ...(.< 000030 00 00 00 00 01 00 00 00 54 00 00 00 54 80 04 08 >........T...T...< 000040 54 80 04 08 04 00 00 00 04 00 00 00 05 00 00 00 >T...............< 000050 00 10 00 00 >....<
  • Para Windows ( * simplemente repite la línea anterior hasta que se alcanza la siguiente dirección * ):
 000000 4d 5a 80 00 01 00 00 00 04 00 10 00 ff ff 00 00 >MZ..............< 000010 40 01 00 00 00 00 00 00 40 00 00 00 00 00 00 00 >@.......@.......< 000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< 000030 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 >................< 000040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 >........!..L.!Th< 000050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f >is program canno< 000060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 >t be run in DOS < 000070 6d 6f 64 65 2e 0d 0a 24 00 00 00 00 00 00 00 00 >mode...$........< 000080 50 45 00 00 4c 01 01 00 ee 71 b4 5e 00 00 00 00 >PE..L....q.^....< 000090 00 00 00 00 e0 00 0f 01 0b 01 01 47 00 02 00 00 >...........G....< 0000a0 00 02 00 00 00 00 00 00 00 10 00 00 00 10 00 00 >................< 0000b0 00 10 00 00 00 00 40 00 00 10 00 00 00 02 00 00 >......@.........< 0000c0 01 00 00 00 00 00 00 00 03 00 0a 00 00 00 00 00 >................< 0000d0 00 20 00 00 00 02 00 00 40 fb 00 00 03 00 00 00 >. ......@.......< 0000e0 00 10 00 00 00 10 00 00 00 00 01 00 00 00 00 00 >................< 0000f0 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 >................< 000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< * 000170 00 00 00 00 00 00 00 00 2e 66 6c 61 74 00 00 00 >.........flat...< 000180 04 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00 >................< 000190 00 00 00 00 00 00 00 00 00 00 00 00 60 00 00 e0 >............`...< 0001a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................< * 000200

Ahora, si agrega su código de máquina a estos encabezados y, para Windows, también agrega un montón de bytes nulos para hacer que el tamaño del archivo sea de 1024 bytes, obtendrá ejecutables válidos que se ejecutarán en el sistema operativo correspondiente.


  1. Suponga ahora que su programa quiere terminar después de hacer una cierta cantidad de cálculos.

    Ahora tiene dos opciones:

    1. Bloqueo: por ejemplo, por la ejecución de una instrucción no válida (en x86 podría ser UD2 ). Esto es fácil, independiente del sistema operativo, pero no elegante.

    2. Pida al sistema operativo que finalice correctamente el proceso. En este punto, necesitamos un mecanismo dependiente del sistema operativo para hacer esto.

En x86 Linux sería

 xor ebx, ebx ; zero exit code mov eax, 1 ; __NR_exit int 0x80 ; do the system call (the easiest way)

En x86 Windows 7 sería

 ; First call terminates all threads except caller thread, see for details: ; http://www.rohitab.com/discuss/topic/41523-windows-process-termination/ mov eax, 0x172 ; NtTerminateProcess_Wind7 mov edx, terminateParams int 0x2e ; do the system call ; Second call terminates current process mov eax, 0x172 mov edx, terminateParams int 0x2e terminateParams: dd 0, 0 ; processHandle, exitStatus

Tenga en cuenta que en otra versión de Windows necesitaría otro número de llamada del sistema. La forma correcta de llamar a NtTerminateProcess es a través de otro matiz de dependencia del sistema operativo: bibliotecas compartidas.


  1. Ahora su programa quiere cargar alguna biblioteca compartida para evitar reinventar algunas ruedas.

Bien, hemos visto que nuestros formatos de archivos ejecutables son diferentes. Supongamos que hemos tenido esto en cuenta y hemos preparado las secciones de importación para el archivo dirigido a cada uno de los sistemas operativos de destino. Todavía hay un problema: la forma de llamar a una función, la llamada convención de llamada, para cada sistema operativo es diferente.

Por ejemplo, suponga que la función de lenguaje C que su programa necesita llamar devuelve una estructura que contiene dos valores int . En Linux, la persona que llama tendría que asignar algo de espacio (por ejemplo, en la pila) y pasarle el puntero como el primer parámetro de la función que se llama, así:

 sub esp, 12 ; 4*2+alignment: stack must be 16-byte aligned push esp ; right before the call instruction call myFunc

En Windows, obtendría el primer valor int de la estructura en EAX y el segundo en EDX , sin pasar ningún parámetro adicional a la función.


Hay otros matices como diferentes esquemas de manipulación de nombres (aunque estos pueden diferir entre compiladores incluso en el mismo sistema operativo), diferentes tipos de datos (por ejemplo, long double en MSVC vs long double en GCC), etc., pero los mencionados anteriormente son los más importantes. diferencias entre los sistemas operativos desde el punto de vista del compilador y el enlazador.

over 4 years ago · Santiago Trujillo Report

0

Estrictamente hablando, no es necesario

Cargadores de programas

Tiene Wine, WSL1 o Darling, que son todos cargadores para los formatos binarios de los otros sistemas operativos respectivos. Estas herramientas funcionan tan bien porque la máquina es básicamente la misma.

Cuando crea un ejecutable, el código de máquina para "5 + 3" es básicamente el mismo en todas las plataformas basadas en x86, sin embargo, existen diferencias, ya mencionadas por las otras respuestas, como:

  • formato de archivo
  • API: ej. Funciones expuestas por el sistema operativo
  • ABI: diseño binario, etc.

Estos difieren. Ahora, por ej. wine hace que Linux comprenda el formato WinPE y luego "simplemente" ejecuta el código de la máquina como un proceso de Linux (¡sin emulación!). Implementa partes de WinAPI y las traduce para Linux. En realidad, Windows hace más o menos lo mismo, ya que los programas de Windows no se comunican con el kernel de Windows (NT), sino con el subsistema Win32... que traduce la WinAPI a la API de NT. Como tal, el vino es "básicamente" otra implementación de WinAPI basada en la API de Linux.

C en una máquina virtual

Además, en realidad puede compilar C en algo más que el código de máquina "desnudo", como el código LLVM Byte o wasm. Proyectos como GraalVM hacen incluso posible ejecutar C en la máquina virtual de Java: Compile una vez, ejecute en todas partes. Allí apunta a otra API/ABI/formato de archivo que estaba destinado a ser "portátil" desde el principio.

Entonces, mientras que el ISA constituye todo el lenguaje que una CPU puede entender, la mayoría de los programas no solo "dependen" del ISA de la CPU, sino que también necesitan que el sistema operativo funcione. La cadena de herramientas debe encargarse de eso

Pero usted está en lo correcto

En realidad, estás bastante cerca de tener razón, sin embargo. De hecho, podría compilar para Linux y Win32 con su compilador y tal vez incluso obtener el mismo resultado, para una definición bastante estrecha de "compilador". Pero cuando invocas al compilador de esta manera:

 c99 -o foo foo.c

No solo compila (traduce el código C a, por ejemplo, ensamblaje), sino que hace esto:

  1. Ejecute el preprocesador C
  2. Ejecute la interfaz del compilador C "real"
  3. Ejecutar el ensamblador
  4. Ejecutar el enlazador

Puede haber más o menos pasos, pero esa es la canalización habitual. Y el paso 2 es, de nuevo con un grano de sal, básicamente el mismo en todas las plataformas. Sin embargo, el preprocesador copia diferentes archivos de encabezado en su unidad de compilación (paso 1) y el enlazador funciona de manera completamente diferente. La traducción real de un idioma (C) a otro (ASM), que es lo que hace un compilador desde una perspectiva teórica, es independiente de la plataforma.

over 4 years ago · Santiago Trujillo Report

0

Para que un binario funcione correctamente (o en algunos casos) hay muchos detalles desagradables que deben ser consistentes/correctos, incluidos, entre otros, probablemente.

  • Cómo las construcciones de origen C, como llamadas a procedimientos, parámetros, tipos, etc., se asignan a construcciones específicas de la arquitectura, como registros, ubicaciones de memoria, marcos de pila, etc.
  • Cómo se expresan los resultados de la compilación en un archivo ejecutable para que el cargador binario pueda cargarlos en los lugares correctos en el espacio de direcciones virtuales y/o realizar "reparaciones" después de que se carguen en un lugar arbitrario.
  • Cómo se implementa exactamente la biblioteca estándar, a veces las funciones de biblioteca estándar son funciones reales en una biblioteca, pero a menudo son macros, funciones en línea o incluso componentes integrados del compilador que pueden depender de funciones no estándar en la biblioteca.
  • Donde se considera que es el límite entre el sistema operativo y la aplicación, en sistemas similares a Unix, la biblioteca estándar de C se considera una biblioteca de plataforma central. Por otro lado, en Windows, la biblioteca estándar de C se considera algo que proporciona el compilador y se compila en la aplicación o se envía junto con ella.
  • ¿Cómo se implementan otras bibliotecas? que nombres usan como se cargan

Las diferencias en una o más de estas cosas son la razón por la que no puede simplemente tomar un binario destinado a un sistema operativo y cargarlo normalmente en otro.

Dicho esto, es posible ejecutar código destinado a un sistema operativo en otro. Eso es esencialmente lo que hace el vino. Tiene bibliotecas traductoras especiales que traducen las llamadas a la API de Windows en llamadas que están disponibles en Linux y un cargador binario especial que sabe cómo cargar archivos binarios tanto de Windows como de Linux.

over 4 years ago · Santiago Trujillo Report

0

No, no solo está apuntando a una CPU. También está apuntando al sistema operativo. Digamos que necesita imprimir algo en la pantalla del terminal usando cout . cout eventualmente terminará llamando a una función API para el sistema operativo en el que se ejecuta el programa. Esa llamada puede ser, y será, diferente para diferentes sistemas operativos, lo que significa que debe compilar el programa para cada sistema operativo para que realice las llamadas de sistema operativo correctas.

over 4 years ago · Santiago Trujillo Report

0

¿Cómo asignas la memoria? No hay instrucciones de CPU para asignar memoria dinámica, debe solicitar la memoria al sistema operativo. Pero, ¿cuáles son los parámetros? ¿Cómo se invoca el sistema operativo?

¿Cómo se imprime la salida? ¿Cómo abres un archivo? ¿Cómo se configura un temporizador? ¿Cómo se muestra una interfaz de usuario? Todas estas cosas requieren solicitar servicios del sistema operativo, y diferentes sistemas operativos brindan diferentes servicios con diferentes llamadas necesarias para solicitarlos.

over 4 years ago · Santiago Trujillo Report

0

  1. La biblioteca estándar y el tiempo de ejecución de C deben interactuar con las API del sistema operativo.
  2. Los formatos ejecutables para los diferentes sistemas operativos de destino son diferentes.
  3. Los diferentes kernels del sistema operativo pueden configurar el hardware de manera diferente. Cosas como el orden de los bytes, la dirección de la pila, las convenciones de uso de registros y probablemente muchas otras cosas pueden ser físicamente diferentes.
over 4 years ago · Santiago Trujillo Report

0

¿No apuntamos a la arquitectura/conjunto de instrucciones de la CPU al compilar un programa C/C++?

No, no lo haces.

Quiero decir que sí, estás compilando para un conjunto de instrucciones de CPU. Pero eso no es todo lo que es la compilación.

Considere el más simple "¡Hola, mundo!" programa. Todo lo que hace es llamar a printf , ¿verdad? Pero no hay un código de operación de conjunto de instrucciones "printf". Entonces... ¿qué sucede exactamente?

Bueno, eso es parte de la biblioteca estándar de C. Su función printf realiza algún procesamiento en la cadena y los parámetros, luego... lo muestra. ¿Cómo sucede eso? Bueno, envía la cadena a la salida estándar. Bien... ¿quién controla eso?

El sistema operativo. Y tampoco hay un código de operación de "salida estándar", por lo que enviar una cadena a la salida estándar implica algún tipo de llamada al sistema operativo.

Y las llamadas al sistema operativo no están estandarizadas en todos los sistemas operativos. Prácticamente todas las funciones de biblioteca estándar que hacen algo que no podría crear por su cuenta en C o C++ se comunicarán con el sistema operativo para hacer al menos parte de su trabajo.

malloc ? La memoria no te pertenece; pertenece al sistema operativo, y tal vez se le permita tener algunos. scanf ? La entrada estándar no te pertenece; pertenece al sistema operativo, y tal vez puedas leerlo. Y así.

Su biblioteca estándar se crea a partir de llamadas a rutinas del sistema operativo. Y esas rutinas del sistema operativo no son portátiles, por lo que la implementación de su biblioteca estándar no es portátil. Entonces, su ejecutable tiene estas llamadas no portátiles.

Y además de todo eso, los diferentes sistemas operativos tienen diferentes ideas de cómo se ve un "ejecutable". Después de todo, un ejecutable no es solo un montón de códigos de operación; ¿Dónde crees que se almacenan todas esas variables static constantes y preiniciadas? Los diferentes sistemas operativos tienen diferentes formas de iniciar un ejecutable, y la estructura del ejecutable es parte de eso.

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!