Estou tentando determinar os detalhes técnicos de por que o software produzido usando linguagens de programação para determinados sistemas operacionais só funciona com eles.
Entendo que os binários são específicos para determinados processadores devido à linguagem de máquina específica do processador que eles compreendem e aos diferentes conjuntos de instruções entre diferentes processadores. Mas de onde vem a especificidade do sistema operacional? Eu costumava assumir que eram APIs fornecidas pelo sistema operacional, mas depois vi esse diagrama em um livro:
Sistemas operacionais - princípios internos e de design 7ª ed - W. Stallings (Pearson, 2012)
Como você pode ver, as APIs não são indicadas como parte do sistema operacional.
Se, por exemplo, eu criar um programa simples em C usando o seguinte código:
#include<stdio.h>
main()
{
printf("Hello World");
}
O compilador está fazendo algo específico do SO ao compilar isso?
fonte
printf
from msvcr90.dll não é o mesmo queprintf
libc.so.6)Respostas:
Você menciona como se o código é específico para uma CPU, por que também deve ser específico para um sistema operacional. Esta é realmente uma pergunta mais interessante que muitas das respostas aqui assumiram.
Modelo de Segurança da CPU
O primeiro programa executado na maioria das arquiteturas de CPU é executado dentro do que é chamado de anel interno ou anel 0 . O modo como um arco específico da CPU implementa anéis varia, mas parece que quase toda CPU moderna possui pelo menos 2 modos de operação, um privilegiado e que executa código 'bare metal' que pode executar qualquer operação legal que a CPU possa executar e a outra é não confiável e executa código protegido, que só pode executar um conjunto seguro de recursos definido. No entanto, algumas CPUs têm granularidade muito maior e, para usar VMs com segurança, são necessários pelo menos 1 ou 2 toques extras (geralmente rotulados com números negativos), mas isso está além do escopo desta resposta.
Onde o SO entra
SOs iniciais de tarefas únicas
No DOS e em outros sistemas baseados em tarefas únicas, todo o código era executado no anel interno, todos os programas que você executava tinham poder total sobre todo o computador e podiam fazer literalmente qualquer coisa se se comportasse mal, incluindo apagar todos os dados ou danificar o hardware. em alguns casos extremos, como definir modos de exibição inválidos em telas muito antigas, pior ainda, isso pode ser causado por simplesmente código de buggy, sem qualquer malícia.
Na verdade, esse código era praticamente independente do sistema operacional, desde que você tivesse um carregador capaz de carregar o programa na memória (bastante simples para os formatos binários iniciais) e o código não dependesse de drivers, implementando todo o acesso de hardware em que deveria ser executado. qualquer sistema operacional, desde que seja executado no anel 0. Nota, um sistema operacional muito simples como esse geralmente é chamado de monitor se for simplesmente usado para executar outros programas e não oferece funcionalidade adicional.
Sistemas operacionais multitarefa modernos
Sistemas operacionais mais modernos, incluindo UNIX , versões do Windows começando com NT e vários outros sistemas operacionais obscuros decidiram melhorar essa situação, os usuários queriam recursos adicionais , como multitarefa, para que pudessem executar mais de um aplicativo ao mesmo tempo e proteção, um bug ( ou código malicioso) em um aplicativo não poderia mais causar danos ilimitados à máquina e aos dados.
Isso foi feito usando os anéis mencionados acima, o sistema operacional ocuparia o único lugar em execução no anel 0 e os aplicativos executariam nos anéis externos não confiáveis, capazes apenas de executar um conjunto restrito de operações permitidas pelo sistema operacional.
No entanto, esse aumento de utilidade e proteção custava um custo, os programas agora tinham que trabalhar com o sistema operacional para executar tarefas que não tinham permissão para realizar, não podiam mais, por exemplo, assumir o controle direto do disco rígido acessando sua memória e alterando arbitrariamente em vez disso, eles precisavam pedir ao sistema operacional para executar essas tarefas para que pudessem verificar se eles estavam autorizados a executar a operação, não alterando arquivos que não lhes pertenciam, mas também para verificar se a operação era realmente válida e não deixaria o hardware em um estado indefinido.
Cada sistema operacional decidiu uma implementação diferente para essas proteções, parcialmente baseada na arquitetura para a qual o sistema operacional foi projetado e parcialmente baseada no design e nos princípios do sistema operacional em questão; o UNIX, por exemplo, enfatizou que as máquinas são boas para uso multiusuário e focadas. os recursos disponíveis para isso, enquanto o Windows foi projetado para ser mais simples, para rodar em hardware mais lento com um único usuário. A maneira como os programas de espaço do usuário também conversam com o sistema operacional é completamente diferente no X86, como seria no ARM ou MIPS, por exemplo, forçando um sistema operacional de várias plataformas a tomar decisões com base na necessidade de trabalhar no hardware para o qual é direcionado.
Essas interações específicas do SO são geralmente chamadas de "chamadas de sistema" e abrangem como um programa de espaço do usuário interage completamente com o hardware através do SO; elas diferem fundamentalmente com base na função do SO e, portanto, um programa que faz seu trabalho através de chamadas do sistema precisa: seja específico do SO.
O Carregador de Programas
Além das chamadas do sistema, cada sistema operacional fornece um método diferente para carregar um programa da mídia de armazenamento secundário e na memória . Para ser carregado por um sistema operacional específico, o programa deve conter um cabeçalho especial que descreva para o sistema operacional como ele pode ser carregado e executado.
Esse cabeçalho costumava ser simples o suficiente para escrever um carregador para um formato diferente era quase trivial, no entanto, com formatos modernos, como o elf, que oferecem suporte a recursos avançados, como vínculo dinâmico e declarações fracas, agora é quase impossível para um sistema operacional tentar carregar binários que não foram projetados para isso, isso significa que, mesmo que não existissem incompatibilidades de chamada do sistema, é imensamente difícil colocar um programa no ram de maneira que ele possa ser executado.
Bibliotecas
Os programas raramente usam chamadas de sistema diretamente, no entanto, eles ganham quase exclusivamente sua funcionalidade, embora as bibliotecas que envolvem as chamadas de sistema em um formato um pouco mais amigável para a linguagem de programação, por exemplo, C tenha a Biblioteca Padrão C e glibc no Linux e libs similares e win32 em No Windows NT e acima, a maioria das outras linguagens de programação também possui bibliotecas semelhantes que agrupam a funcionalidade do sistema de maneira apropriada.
Essas bibliotecas podem, até certo ponto, superar os problemas de plataforma cruzada, conforme descrito acima, há uma variedade de bibliotecas projetadas para fornecer uma plataforma uniforme para aplicativos enquanto gerencia internamente chamadas para uma ampla variedade de sistemas operacionais , como SDL , isso significa que, embora programas não podem ser compatíveis com binários, programas que usam essas bibliotecas podem ter fontes comuns entre plataformas, tornando a portabilidade tão simples quanto recompilar.
Exceções ao acima
Apesar de tudo o que disse aqui, houve tentativas de superar as limitações de não poder executar programas em mais de um sistema operacional. Alguns bons exemplos são o projeto Wine, que emulou com êxito o carregador de programas win32, o formato binário e as bibliotecas do sistema, permitindo que os programas do Windows sejam executados em vários UNIXes. Há também uma camada de compatibilidade que permite que vários sistemas operacionais BSD UNIX executem o software Linux e, é claro, o próprio calço da Apple, permitindo executar um software MacOS antigo no MacOS X.
No entanto, esses projetos trabalham com níveis enormes de esforço de desenvolvimento manual. Dependendo da diferença entre os dois sistemas operacionais, a dificuldade varia de um calço razoavelmente pequeno até a emulação quase completa do outro sistema operacional, que geralmente é mais complexo do que escrever um sistema operacional inteiro em si e, portanto, essa é a exceção e não a regra.
fonte
Eu acho que você está lendo muito no diagrama. Sim, um sistema operacional especificará uma interface binária de como as funções do sistema operacional são chamadas e também definirá um formato de arquivo para executáveis, mas também fornecerá uma API, no sentido de fornecer um catálogo de funções que podem ser chamadas por um aplicativo para chamar serviços do SO.
Eu acho que o diagrama está apenas tentando enfatizar que as funções do sistema operacional geralmente são chamadas por meio de um mecanismo diferente do que uma simples chamada de biblioteca. A maioria do sistema operacional comum usa interrupções no processador para acessar as funções do sistema operacional. Os sistemas operacionais modernos típicos não permitem que um programa do usuário acesse diretamente nenhum hardware. Se você quiser escrever um personagem no console, precisará solicitar ao sistema operacional que faça isso por você. A chamada do sistema usada para gravar no console variará de sistema operacional para sistema operacional; portanto, há um exemplo de por que o software é específico do sistema operacional.
printf é uma função da biblioteca de tempo de execução C e em uma implementação típica é uma função bastante complexa. Se você pesquisar no Google, poderá encontrar a fonte de várias versões online. Consulte esta página para uma visita guiada a um . Na grama, apesar de acabar fazendo uma ou mais chamadas de sistema, e cada uma dessas chamadas de sistema é específica para o sistema operacional host.
fonte
Provavelmente. Em algum momento durante o processo de compilação e vinculação, seu código é transformado em um binário específico do SO e vinculado a todas as bibliotecas necessárias. Seu programa deve ser salvo em um formato que o sistema operacional espera, para que o SO possa carregar o programa e começar a executá-lo. Além disso, você está chamando a função de biblioteca padrão
printf()
, que em algum nível é implementada em termos dos serviços que o sistema operacional fornece.As bibliotecas fornecem uma interface - uma camada de abstração do sistema operacional e do hardware - e isso possibilita recompilar seu programa para um sistema operacional ou hardware diferente. Mas essa abstração existe no nível da fonte - uma vez que o programa é compilado e vinculado, ele é conectado a uma implementação específica dessa interface, específica para um determinado sistema operacional.
fonte
Há várias razões, mas uma razão muito importante é que o sistema operacional precisa saber como ler as séries de bytes que compõem seu programa na memória, encontrar as bibliotecas que acompanham esse programa e carregá-las na memória, e então comece a executar o código do seu programa. Para fazer isso, os criadores do sistema operacional criam um formato específico para essa série de bytes, para que o código do sistema operacional saiba onde procurar as várias partes da estrutura do seu programa. Como os principais sistemas operacionais têm autores diferentes, esses formatos geralmente têm pouco a ver um com o outro. Em particular, o formato executável do Windows tem pouco em comum com o formato ELF usado pela maioria das variantes do Unix. Portanto, todo esse carregamento, ligação dinâmica e código de execução devem ser específicos do SO.
Em seguida, cada sistema operacional fornece um conjunto diferente de bibliotecas para conversar com a camada de hardware. Essas são as APIs que você mencionou e geralmente são bibliotecas que apresentam uma interface mais simples para o desenvolvedor, convertendo-a em chamadas mais complexas e mais específicas para as profundezas do próprio sistema operacional, essas chamadas geralmente não são documentadas ou protegidas. Essa camada geralmente é bastante cinza, com as APIs "OS" mais recentes criadas parcialmente ou inteiramente em APIs antigas. Por exemplo, no Windows, muitas das APIs mais recentes que a Microsoft criou ao longo dos anos são essencialmente camadas sobre as APIs Win32 originais.
Um problema que não surge no seu exemplo, mas que é um dos maiores que os desenvolvedores enfrentam é a interface com o gerenciador de janelas, para apresentar uma GUI. Se o gerenciador de janelas faz parte do "SO" às vezes depende do seu ponto de vista, assim como do próprio SO, com a GUI no Windows sendo integrada ao SO em um nível mais profundo, enquanto as GUIs no Linux e OS X estão sendo mais diretamente separados. Isso é muito importante, porque hoje o que as pessoas costumam chamar de "O sistema operacional" é um animal muito maior do que o que os livros didáticos tendem a descrever, pois inclui muitos componentes no nível de aplicativos.
Finalmente, não é um problema estritamente do sistema operacional, mas um problema importante na geração de arquivos executáveis é que máquinas diferentes têm destinos de linguagem de montagem diferentes e, portanto, o código do objeto gerado real deve ser diferente. Isso não é estritamente um problema de "SO", mas um problema de hardware, mas significa que você precisará de compilações diferentes para diferentes plataformas de hardware.
fonte
De outra resposta minha:
Portanto, o sistema operacional fornece serviços aos aplicativos para que eles não precisem executar trabalhos redundantes.
Seu programa C de exemplo usa printf, que envia caracteres para stdout - um recurso específico do SO que exibirá os caracteres em uma interface do usuário. O programa não precisa saber onde está a interface do usuário - pode estar no DOS, pode estar em uma janela gráfica, pode ser canalizada para outro programa e usada como entrada para outro processo.
Como o sistema operacional fornece esses recursos, os programadores podem realizar muito mais com pouco trabalho.
No entanto, mesmo iniciar um programa é complicado. O sistema operacional espera que um arquivo executável tenha certas informações no início que digam ao sistema operacional como deve ser iniciado e, em alguns casos (ambientes mais avançados, como Android ou iOS), quais recursos serão necessários e que precisam de aprovação, pois tocam em recursos fora do "sandbox" - uma medida de segurança para ajudar a proteger usuários e outros aplicativos contra programas que se comportam mal.
Portanto, mesmo que o código da máquina executável seja o mesmo e não haja recursos do sistema operacional necessários, um programa compilado para o Windows não será executado em um sistema operacional OS X sem uma camada adicional de emulação ou conversão, mesmo no mesmo hardware exato.
Os sistemas operacionais antigos do DOS geralmente podiam compartilhar programas, porque implementavam a mesma API no hardware (BIOS) e o sistema operacional conectado ao hardware para fornecer serviços. Portanto, se você escreveu e compilou um programa COM - que é apenas uma imagem de memória de uma série de instruções do processador - você pode executá-lo no CP / M, no MS-DOS e em vários outros sistemas operacionais. Na verdade, você ainda pode executar programas COM em máquinas Windows modernas. Outros sistemas operacionais não usam os mesmos ganchos da API do BIOS, portanto, os programas COM não serão executados neles sem, novamente, uma camada de emulação ou conversão. Os programas EXE seguem uma estrutura que inclui muito mais do que meras instruções do processador e, portanto, juntamente com os problemas da API, não é executado em uma máquina que não entende como carregá-lo na memória e executá-lo.
fonte
Na verdade, a resposta real é que, se todo sistema operacional compreendesse o mesmo layout de arquivo binário executável, e você se limitasse apenas a funções padronizadas (como na biblioteca padrão C) fornecidas pelo sistema operacional (que sistemas operacionais fornecem), então o software seria de fato, execute em qualquer sistema operacional.
Claro, a realidade é que não é esse o caso. Um
EXE
arquivo não tem o mesmo formato que umELF
arquivo, mesmo que ambos contenham código binário para a mesma CPU. * Portanto, cada sistema operacional precisaria ser capaz de interpretar todos os formatos de arquivo, e eles simplesmente não fizeram isso no diretório começando, e não havia razão para eles começarem a fazê-lo mais tarde (quase certamente por razões comerciais e não técnicas).Além disso, seu programa provavelmente precisa fazer coisas que a biblioteca C não define como fazer (mesmo para coisas simples, como listar o conteúdo de um diretório) e, nesses casos, todo sistema operacional fornece suas próprias funções para alcançar seu objetivo. tarefa, naturalmente significando que não haverá um denominador comum mais baixo para você usar (a menos que você mesmo faça esse denominador).
Então, em princípio, é perfeitamente possível. De fato, o WINE executa executáveis do Windows diretamente no Linux.
Mas é uma tonelada de trabalho e (geralmente) comercialmente injustificada.
* Nota: Há muito mais em um arquivo executável do que apenas código binário. Há uma tonelada de informações que informa o sistema operacional quais bibliotecas o arquivo depende, quanto pilha de memória que precisa, as funções que exporta para outras bibliotecas que pode depender dela, onde o sistema operacional pode encontrar informações de depuração relevante, como " re-localize "o arquivo na memória, se necessário, como fazer com que o tratamento de exceções funcione corretamente, etc. etc .... novamente, pode haver um formato único para o qual todos concordam, mas simplesmente não existe.
fonte
O diagrama tem a camada "aplicativo" (principalmente) separada da camada "sistema operacional" pelas "bibliotecas" e isso implica que "aplicativo" e "SO" não precisam saber um do outro. Isso é uma simplificação no diagrama, mas não é bem verdade.
O problema é que a "biblioteca" tem realmente três partes: a implementação, a interface para o aplicativo e a interface para o sistema operacional. Em princípio, os dois primeiros podem ser "universais" no que diz respeito ao sistema operacional (depende de onde você o divide), mas a terceira parte - a interface para o sistema operacional - geralmente não pode. A interface do sistema operacional dependerá necessariamente do sistema operacional, das APIs fornecidas, do mecanismo de empacotamento (por exemplo, o formato do arquivo usado pela DLL do Windows) etc.
Como a "biblioteca" geralmente é disponibilizada como um único pacote, significa que, uma vez que o programa escolhe uma "biblioteca" para uso, ele se compromete com um sistema operacional específico. Isso acontece de duas maneiras: a) o programador escolhe completamente antecipadamente e, em seguida, a ligação entre a biblioteca e o aplicativo pode ser universal, mas a própria biblioteca está vinculada ao sistema operacional; ou b) o programador configura as coisas para que a biblioteca seja selecionada quando você executa o programa, mas o mecanismo de ligação em si, entre o programa e a biblioteca, depende do sistema operacional (por exemplo, o mecanismo DLL no Windows). Cada um tem suas vantagens e desvantagens, mas de qualquer forma você deve fazer uma escolha com antecedência.
Agora, isso não significa que é impossível fazer isso, mas você precisa ser muito inteligente. Para superar o problema, seria necessário escolher a biblioteca em tempo de execução e criar um mecanismo de ligação universal que não dependa do sistema operacional (portanto, você é responsável por mantê-lo, muito mais trabalho). Algumas vezes vale a pena.
Você não precisa, mas se você se esforçar para fazer isso, há uma boa chance de você não querer estar vinculado a um processador específico também, então você escreverá uma máquina virtual e compilará seu programa para um formato de código neutro do processador.
Até agora você já deve ter notado para onde estou indo. Plataformas de linguagem como Java fazem exatamente isso. O Java runtime (biblioteca) define a ligação neutra ao SO entre seu programa Java e a biblioteca (como o Java runtime abre e executa seu programa) e fornece uma implementação específica para o SO atual. O .NET faz a mesma coisa até certo ponto, exceto que a Microsoft não fornece uma "biblioteca" (tempo de execução) para nada além do Windows (mas outros fornecem - consulte Mono). E, na verdade, o Flash também faz a mesma coisa, embora seu escopo seja mais limitado ao Navegador.
Por fim, existem maneiras de fazer a mesma coisa sem um mecanismo de ligação personalizado. Você pode usar ferramentas convencionais, mas adie a etapa de ligação à biblioteca até que o usuário escolha o sistema operacional. É exatamente o que acontece quando você distribui o código fonte. O usuário pega o seu programa e o vincula ao processador (compile) e SO (vincule) quando o usuário estiver pronto para executá-lo.
Tudo depende de como você divide as camadas. No final do dia, você sempre tem um dispositivo de computação fabricado com hardware específico executando código de máquina específico. As camadas existem em grande parte como uma estrutura conceitual.
fonte
O software nem sempre é específico do SO. O Java e o sistema de código p anterior (e até o ScummVM) permitem software portátil nos sistemas operacionais. A Infocom (fabricantes do Zork e da Z-machine ) também tinha um banco de dados relacional baseado em outra máquina virtual. No entanto, em algum nível, algo precisa traduzir essas abstrações em instruções reais a serem executadas em um computador.
fonte
Você diz
Mas o programa que você fornece como exemplo funcionará em muitos sistemas operacionais e até em alguns ambientes bare-metal.
O importante aqui é a distinção entre o código fonte e o binário compilado. A linguagem de programação C foi projetada especificamente para ser independente do SO na forma de origem. Isso é feito deixando a interpretação de coisas como "imprimir no console" até o implementador. Mas C pode estar em conformidade com algo que é específico do SO (consulte outras respostas por razões). Por exemplo, os formatos executáveis PE ou ELF.
fonte
Outras pessoas abordaram bem os detalhes técnicos, gostaria de mencionar uma razão menos técnica, o lado UX / UI:
Escreva uma vez, sinta-se estranho em todos os lugares
Todo sistema operacional possui suas próprias APIs de interface com o usuário e padrões de design. É possível escrever uma interface de usuário para um programa e executá-la em vários sistemas operacionais, mas isso garante que o programa pareça deslocado em qualquer lugar. Para criar uma boa interface do usuário, é necessário ajustar os detalhes de cada plataforma suportada.
Muitos desses são pequenos detalhes, mas eles os enganam e você frustrará seus usuários:
Mesmo quando é tecnicamente possível escrever uma base de código da interface do usuário que seja executada em qualquer lugar, é melhor fazer ajustes para cada sistema operacional suportado.
fonte
Uma distinção importante neste momento é separar o compilador do vinculador. O compilador provavelmente produz mais ou menos a mesma saída (as diferenças são devidas principalmente a vários
#if WINDOWS
s). O vinculador, por outro lado, precisa lidar com todo o material específico da plataforma - vinculando as bibliotecas, criando o arquivo executável etc.Em outras palavras, o compilador se preocupa principalmente com a arquitetura da CPU, porque está produzindo o código executável real e precisa usar as instruções e os recursos da CPU (observe que o IL do .NET ou o bytecode da JVM do .NET seriam considerados conjuntos de instruções de uma CPU virtual Nesta visão). É por isso que você deve compilar o código separadamente para
x86
eARM
, por exemplo.O vinculador, por outro lado, precisa pegar todos esses dados e instruções brutos e colocá-lo em um formato que o carregador (atualmente, esse quase sempre seja o sistema operacional) possa entender, além de vincular bibliotecas vinculadas estaticamente (que também inclui o código necessário para vinculação dinâmica, alocação de memória etc.).
Em outras palavras, você pode compilar o código apenas uma vez e executá-lo no Linux e no Windows - mas é necessário vinculá- lo duas vezes, produzindo dois executáveis diferentes. Agora, na prática, muitas vezes você também precisa fazer concessões no código (é aí que as diretivas do (pré-) compilador entram), então até compilar um link uma vez duas vezes não é muito usado. Sem mencionar que as pessoas estão tratando a compilação e o link como uma única etapa durante a compilação (assim como você não se importa mais com as partes do próprio compilador).
O software da era DOS costumava ser mais binário-portátil, mas você deve entender que ele também foi compilado não no DOS ou no Unix, mas em um contrato que era comum à maioria dos PCs no estilo IBM - descarregando o que hoje são chamadas de API para software interrompe. Isso não precisava de vinculação estática, pois você só precisava definir os registros necessários, por exemplo, chamar
int 13h
funções gráficas, e a CPU saltou para um ponteiro de memória declarado na tabela de interrupção. É claro que, novamente, a prática foi muito mais complicada, porque para obter um desempenho do pedal do metal, era necessário escrever todos esses métodos, mas isso basicamente equivalia a percorrer o sistema operacional por completo. E, claro, há algo que invariavelmente precisa de interação com a API do SO - encerramento do programa. Mas ainda assim, se você usou os formatos mais simples disponíveis (por exemplo,COM
no DOS, que não tem cabeçalho, apenas instruções) e não queria sair, boa sorte! E, é claro, você também pode lidar com a terminação adequada no tempo de execução, para poder ter código para terminação Unix e DOS no mesmo executável e detectar no tempo de execução qual usar :)fonte