Lenguajes Interpretados: ejecución directa de código máquina o bytecode?

A partir de distintas fuentes de información y contenidos básicos que se enseñan en materias de Informática en Ingeniería, surgió la necesidad de cuestionar la afirmación: “lenguaje interpretado es aquel en que un programa intérprete, ejecuta línea a línea el contenido del o los archivos de código fuente, traduciendo a código máquina, sin obtener un código objeto permanente, ya que dicha traducción se pierde al finalizar el programa. Por lo que existe sólo una ejecución por cada compilación.”
Se comienza por analizar un programa escrito en C, para el que se obtienen versión compilada (código ejecutable utilizando ABI compatible con sistema operativo) y versión de código ensamblador, antes de ensamblado (ejecución de GCC “as”) y vinculación.
De este código, se busca mostrar qué sería “lenguaje de máquina”, que se verá que son expresiones formadas por los mnemónicos del set de instrucciones del CPU en cuestión, junto con llamadas a funciones de bibliotecas de sistema (dinámicas o estáticas) y llamadas a funciones del sistema operativo, realizadas directamente (en el código binario del programa) o indirectamente a través de las llamadas a funciones de bibliotecas de sistema.
Se continúa analizando tres lenguajes interpretados “Open Source”, para los cuáles no se suele decir que sus intérpretes funcionan como una máquina virtual, mostrando que el código fuente de las aplicaciones se compila a un “bytecode” intermedio, que son instrucciones de esta máquina virtual, y que en los mismos no existe uso del lenguaje de máquina como se definió para código ejecutable del sistema operativo, sino uso de funcionalidad de esta “máquina virtual”, a través de una API implementada en bibliotecas de sistema.

 

1. Programa en C – Código ejecutable compatible con la plataforma de hardware y software.

Sea el siguiente programa en C, que cuenta hasta 10.

loop_test.c:
#include 
#include 

int main(void)
{
  int i=0;

  // Lazo de cuenta de 1 a 10
  for (i=0; i<10; ++i)
    printf("i: %i\n", i);

  exit(0);
}

Cuando se compila y ejecuta el programa:

$ gcc -o loop_test -g -Wall loop_test.c

$ ./loop_test

i: 0
 i: 1
 i: 2
 i: 3
 i: 4
 i: 5
 i: 6
 i: 7
 i: 8
 i: 9

Se verifica tipo de archivo ejecutable:

$ file loop_test
loop_test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=489c98d75049970ddf5690f363a42506a63da2c9, not
 stripped

Se genera código ensamblador (deteniendo compilación antes del ensamblado y vinculación):

 

$ gcc -s -g -Wall loop_test.c -o loop_test

En código ensamblador, Intel x86, formato AT-T, (loop_test.s)

$ file loop_test.s
loop_test.s: assembler source, ASCII text

Analizando la sección en que se ejecuta el lazo de conteo “for” de función main():

.L3:
        .loc 1 10 0 discriminator 3
        subl    $8, %esp
        pushl   -12(%ebp)
        pushl   $.LC0
        call    printf // llamada a printf(), argumentos en stack
        addl    $16, %esp
        .loc 1 9 0 discriminator 3
        addl    $1, -12(%ebp)
.L2:
        .loc 1 9 0 is_stmt 0 discriminator 1
        // comparación entre argumento y variable en stack (i)
        cmpl    $9, -12(%ebp) 
        // salta a L3 mientras sea menor al valor en $9
        jle     .L3
        .loc 1 12 0 is_stmt 1
       // Si no fue menor, cargaargumento a exit() y llama a exit
       subl    $12, %esp
        pushl   $0
        call    exit
       .cfi_endproc

Puede verse que en el listado del programa, efectivamente se están utilizando:

– Mnemónicos del set de instrucciones del CPU 80386, como ser: jle, subl, pushl, addl, call, cmpl,
– Llamadas a funciones de biblioteca C: exit, printf.

¿En qué punto se realizan llamadas al sistema operativo?

Se utiliza para ello el programa “strace”: analizador de cómo el programa interactúa con el sistema operativo.

Véase también:

www.thegeekstuff.com/2011/11/strace-examples/

medium.com/@lizrice/go-and-missing-processes-77b6341637ec

 
$ strace ./loop_test 1>/dev/null

...

ioctl(1,TCGETS, 0xbfeeec4c) = -1 ENOTTY (Inappropriate ioctl for device)

brk(NULL)= 0x8234000

brk(0x8256000)= 0x8256000

write(1,"i: 0\ni: 1\ni: 2\ni: 3\ni: 4\ni: 5\ni:"..., 50) = 50

exit_group(0)= ?

+++exited with 0 +++

 

ioctl, write y exit_group son llamadas de sistema (kernel de sistema operativo), escritas por “strace” en forma simbólica.
Se agrega “1>/dev/null” para redirigir la salida normal de programa al dispositivo nulo, de forma que no interfiera con el listado de “strace”. Normalmente, durante la depuración de un programa, no se quiere que esto ocurra.

Mostrando sólo determinadas llamadas de sistema:

$ strace -e trace=write,open,read ./loop_test 1>/dev/null

open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\200\207\1\0004\0\0\0"..., 512) = 512

write(1, "i: 0\ni: 1\ni: 2\ni: 3\ni: 4\ni: 5\ni:"..., 50) = 50

+++exited with 0 +++


Se puede apreciar que:

Las bibliotecas dinámicas se acceden a partir de la base de datos de bibliotecas compartidas, en /etc/ld.so.cache, actualizada mediante utilidad de sistema “ldconfig”.
El programa hace uso de la biblioteca C (GNU Lib C) de sistema, “/lib/i386-linux-gnu/libc.so.6”

2. Programa en Lúa – Código fuente a ser interpretado y ejecutado como…


Sea el siguiente programa en Lua:

lua_loop.lua:
#!/usr/bin/lua

vect_a={};

j=1;
vect_size=4;

print(arg[0]..": vector of fractions of PI radians\n");

for j=1,vect_size do
  vect_a[j]=math.pi/j;
  -- print() agrega automáticamente una nueva línea...
  print(arg[0]..": pi/"..j..": "..vect_a[j]);
end

os.exit(0);

Datos del intérprete:

$ which lua

/usr/bin/lua

$ lua -v

Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
Datos del compilador (a bytecode “Lua”)
$ luac -v

Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
Compilando el programa:
$ luac -o lua_loop.luac ./lua_loop.lua
Datos del “bytecode” obtenido:
$ file lua_loop.luac
lua_loop.luac:
 Lua bytecode, version 5.1

Ejecutando el programa:

$ lua lua_loop.luac

lua_loop.luac: vector of fractions of PI radians

lua_loop.luac: pi/1: 3.1415926535898

lua_loop.luac: pi/2: 1.5707963267949

lua_loop.luac: pi/3: 1.0471975511966

lua_loop.luac: pi/4: 0.78539816339745

En este punto, se hace necesaria la disponibilidad de un “decompilador” o analizador de “bytecode” Lua. Tener en cuenta que se forzó generación de “bytecode” para explicitar lo que el intérprete Lua realiza al vuelo cada vez que se lo invoca con un código fuente para ejecutar.
El disponer de los archivos de bytecode acelerará la ejecución.
Luadec
https://github.com/viruscamp/luadec
Luego de realizar clonado y compilación de esta herramienta (dependencias, además de paquete de desarrollo de Lua, de Ncurses y LibReadline), se intenta decompilar archivo de “bytecode”
Se intenta primero “decompilación”.

$ ~/src/luadec/luadec/luadec lua_loop.luac
--
 Decompiled using luadec 2.2 rev: 895d923 for Lua 5.1 from
 https://github.com/viruscamp/luadec
--
 Command line: lua_loop.luac 

-- params : ...

-- function num : 0

vect_a={}

j = 1

vect_size= 4

print(arg[0].. ": vector of fractions of PI radians\n")

for j =1, vect_size do

 -- DECOMPILER ERROR at PC20: Confused about usage of register: R4 in 'UnsetPending'

 vect_a[j]= math.pi / j

 print(arg[0].. ": pi/" .. j .. ": " .. vect_a[j])

end;

(os.exit)(0)

 

Probando desensamblado:

$ ~/src/luadec/luadec/luadec -dis lua_loop.luac

;
 Disassembled using luadec 2.2 rev: 895d923 for Lua 5.1 from
 https://github.com/viruscamp/luadec
;
 Command line: -dis lua_loop.luac 
; Function: 0
; Defined at line: 0
; #Upvalues: 0
; #Parameters: 0
; Is_vararg: 2
; Max Stack Size: 10
0 [-]: NEWTABLE R0 0 0 ; R0 := {} (size = 0,0)
1 [-]: SETGLOBAL R0 K0 ; vect_a := R0
2 [-]: LOADK R0 K2 ; R0 := 1
3 [-]: SETGLOBAL R0 K1 ; j := R0
4 [-]: LOADK R0 K4 ; R0 := 4
5 [-]: SETGLOBAL R0 K3 ; vect_size := R0
6 [-]: GETGLOBAL R0 K5 ; R0 := print
7 [-]: GETGLOBAL R1 K6 ; R1 := arg
8 [-]: GETTABLE R1 R1 K7 ; R1 := R1[0]
9 [-]: LOADK R2 K8 ; R2 := ": vector of fractions of PI radians\n"
10 [-]: CONCAT R1 R1 R2 ; R1 := concat(R1 to R2)
11 [-]: CALL R0 2 1 ; := R0(R1)
12 [-]: LOADK R0 K2 ; R0 := 1
13 [-]: GETGLOBAL R1 K3 ; R1 := vect_size
14 [-]: LOADK R2 K2 ; R2 := 1
15 [-]: FORPREP R0 15 ; R0 -= R2; pc += 15 (goto 31)
16 [-]: GETGLOBAL R4 K0 ; R4 := vect_a
17 [-]: GETGLOBAL R5 K9 ; R5 := math
18 [-]: GETTABLE R5 R5 K10 ; R5 := R5["pi"]
19 [-]: DIV R5 R5 R3 ; R5 := R5 / R3
20 [-]: SETTABLE R4 R3 R5 ; R4[R3] := R5
21 [-]: GETGLOBAL R4 K5 ; R4 := print
22 [-]: GETGLOBAL R5 K6 ; R5 := arg
23 [-]: GETTABLE R5 R5 K7 ; R5 := R5[0]
24 [-]: LOADK R6 K11 ; R6 := ": pi/"
25 [-]: MOVE R7 R3 ; R7 := R3
26 [-]: LOADK R8 K12 ; R8 := ": "
27 [-]: GETGLOBAL R9 K0 ; R9 := vect_a
28 [-]: GETTABLE R9 R9 R3 ; R9 := R9[R3]
29 [-]: CONCAT R5 R5 R9 ; R5 := concat(R5 to R9)
30 [-]: CALL R4 2 1 ; := R4(R5)
31 [-]: FORLOOP R0 -16 ; R0 += R2; if R0 <= R1 then R3 := R0; PC += -16 , goto 16 end
32 [-]: GETGLOBAL R0 K13 ; R0 := os
33 [-]: GETTABLE R0 R0 K14 ; R0 := R0["exit"]
34 [-]: LOADK R1 K7 ; R1 := 0
35 [-]: CALL R0 2 1 ; := R0(R1)
36 [-]: RETURN R0 1 ; return

Puede observarse claramente que las instrucciones que ejecuta el intérprete no corresponden a lenguaje máquina del CPU 80386 en el que está ejecutando el sistema operativo (como tampoco el espacio de direcciones y tipos de registros), sino que corresponden al “lenguaje máquina” de la máquina virtual de Lua.

Es la VM de Lua la que estará ejecutando instrucciones de lenguaje de máquina, pero no en relación directa con las líneas interpretadas y en ejecución del programa de usuario, como se verá en el caso siguiente, con el mismo lazo implementado en Python.

 

3. Programa en Python – Código fuente interpretado y ejecutado como…

Siguiendo con la línea de trabajo planteada, sea el siguiente programa en Python v2.7:

 
py_loop.py:
........................................
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Facilidades de sistema
import sys
import math

vect_len=4
vect_a=[]

for i in range(vect_len):
  vect_a.append(math.pi/(i+1))
  sys.stdout.write("vect_a["+str(i)+"]: "+str(vect_a[i])+" radians\n")

sys.exit(0)

Datos del entorno Python:

$ python --version

Python 2.7.11

Ejecutando el programa:

$ python ./py_loop.py 

vect_a[0]: 3.14159265359 radians

vect_a[1]: 1.57079632679 radians

vect_a[2]: 1.0471975512 radians

vect_a[3]: 0.785398163397 radians

Explicitando “bytecode” Python:

$ pycompile --version

pycompile 1.0

$ pycompile -v py_loop.py

D: pycompile:214: argv: ['/usr/bin/pycompile', '-v', 'py_loop.py']

D: pycompile:215: options: {'force': False, 'verbose': True, 'package':
 None, 'regexpr': None, 'vrange': None, 'optimize': False}

D: pycompile:216: args: ['py_loop.py']

Verificando tipos de archivos:

$ file py_loop.py

py_loop.py: Python script, UTF-8 Unicode text executable

$ file py_loop.pyc

py_loop.pyc: python 2.7 byte-compiled
Ejecutando “bytecode”:
$ ./py_loop.pyc

vect_a[0]: 3.14159265359 radians

vect_a[1]: 1.57079632679 radians

vect_a[2]: 1.0471975512 radians

vect_a[3]: 0.785398163397 radians

Nuevamente, se necesita de herramienta específica para desensamblar “bytecode”.

Desensamblador para Python bytecode – Lib/dis.py

https://docs.python.org/2/library/dis.html
Python dispone de desensamblador de módulos en línea, módulo “dis”. Se hacen las siguientes observaciones:
“The CPython bytecode which this module takes as an input is defined in the file Include/opcode.h and used by the compiler and the interpreter.”
“CPython implementation detail: Bytecode is an implementation detail of the CPython interpreter! No guarantees are made that bytecode will not be added, removed, or changed between versions of Python. Use of this module should not be considered to work across Python VMs or Python releases.”

Este módulo trabaja también sobre archivos “py”, presentando a “stdout” las instrucciones de la Python VM que resultarán de ejecutar el programa.

Ejemplo: se desensambla “py_loop.py”

$ python -m dis py_loop.py
5 0 LOAD_CONST 0 (-1)
3 LOAD_CONST 1 (None)
6 IMPORT_NAME 0 (sys)
9 STORE_NAME 0 (sys)
6 12 LOAD_CONST 0 (-1)
15 LOAD_CONST 1 (None)
18 IMPORT_NAME 1 (math)
21 STORE_NAME 1 (math)
11 24 LOAD_CONST 2 (4)
27 STORE_NAME 2 (vect_len)
12 30 BUILD_LIST 0
33 STORE_NAME 3 (vect_a)
14 36 SETUP_LOOP 92 (to 131)
39 LOAD_NAME 4 (range)
42 LOAD_NAME 2 (vect_len)
45 CALL_FUNCTION 1
48 GET_ITER 
>>
49 FOR_ITER 78 (to 130)
52 STORE_NAME 5 (i)
15 55 LOAD_NAME 3 (vect_a)
58 LOAD_ATTR 6 (append)
61 LOAD_NAME 1 (math)
64 LOAD_ATTR 7 (pi)
67 LOAD_NAME 5 (i)
70 LOAD_CONST 3 (1)
73 BINARY_ADD 
74 BINARY_DIVIDE 
75 CALL_FUNCTION 1
78 POP_TOP 
16 79 LOAD_NAME 0 (sys)
82 LOAD_ATTR 8 (stdout)
85 LOAD_ATTR 9 (write)
88 LOAD_CONST 4 ('vect_a[')
91 LOAD_NAME 10 (str)
94 LOAD_NAME 5 (i)
97 CALL_FUNCTION 1
100 BINARY_ADD 
101 LOAD_CONST 5 (']: ')
104 BINARY_ADD
105 LOAD_NAME 10 (str)
108 LOAD_NAME 3 (vect_a)
111 LOAD_NAME 5 (i)
114 BINARY_SUBSCR 
115 CALL_FUNCTION 1
118 BINARY_ADD 
119 LOAD_CONST 6 (' radians\n')
122 BINARY_ADD 
123 CALL_FUNCTION 1
126 POP_TOP
127 JUMP_ABSOLUTE 49
>>
130 POP_BLOCK 

18 >>
131 LOAD_NAME 0 (sys)
134 LOAD_ATTR 11 (exit)
137 LOAD_CONST 7 (0)
140 CALL_FUNCTION 1
143 POP_TOP 
144 LOAD_CONST 1 (None)
147 RETURN_VALUE

Puede observarse nuevamente que las instrucciones que ejecuta el intérprete no corresponden a lenguaje máquina del CPU 80386 en el que está ejecutando el sistema operativo, sino que corresponden al “lenguaje máquina” de la máquina virtual de Python.
En una segunda instancia, el intérprete Python si debe realizar llamadas a funciones de sistema necesarias, respetando los tiempos indicados en el código fuente. Por lo que no hay una relación directa entre líneas interpretadas e instrucciones de
lenguaje máquina.

Para visualizar mejor este hecho, se modifica código fuente como sigue:

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Facilidades de sistema
import sys
import math

vect_len=4
vect_a=[]

for i in range(vect_len):
  vect_a.append(math.pi/(i+1))
  sys.stdout.write("vect_a["+str(i)+"]: "+str(vect_a[i])+" radians\n")
  sys.stdout.flush() # Fuerza llamada a “write” hacia consola.

sys.exit(0)

Se utiliza “strace” para verificar cuántas veces se realiza la llamada de sistema:

$ strace -e trace=write python ./py_loop.py 1>/dev/null

write(1, "vect_a[0]: 3.14159265359 radians"..., 33) = 33

write(1, "vect_a[1]: 1.57079632679 radians"..., 33) = 33

write(1, "vect_a[2]: 1.0471975512 radians\n", 32) = 32

write(1, "vect_a[3]: 0.785398163397 radian"..., 34) = 34

+++ exited with 0 +++

En cada repetición del lazo “for”, la VM de Python respeta la secuencia solicitada y llama a “write” cada vez que se solicitó un “flush” del descriptor asociado a “STDOUT”, que en este caso resulta ser el terminal.

4. Programa en Ruby – Código fuente interpretado y ejecutado como…

Sea el siguiente código fuente en Ruby, que implementa el mismo lazo de conteo ya programado en Lua y Python:

rb_loob.rb:
#!/usr/bin/ruby
# encoding: UTF-8
# http://ruby-doc.org/core-2.4.1/Encoding.html

$i=0
$vect_len=4

$vect_a=[ 0.0 ]

# https://ruby-doc.org/core-2.2.0/Math.html
# STDOUT.flush
# https://www.ruby-forum.com/topic/208856

for i in 1..$vect_len
  $vect_a[$i]=Math::PI/i
  puts "vect_a[#{$i}]: #{$vect_a[$i]}\n"
  STDOUT.flush
end
Datos de intérprete Ruby:
$ ruby --version

ruby 2.2.3p173 (2015-08-18) [i386-linux-gnu]

Ejecutando el programa:

$ ./rb_loop.rb 

vect_a[0]: 3.141592653589793

vect_a[0]: 1.5707963267948966

vect_a[0]: 1.0471975511965976

vect_a[0]: 0.7853981633974483

En Ruby, nuevamente, la ejecución se realiza a través de una VM, la YARV:
https://en.wikipedia.org/wiki/YARV
Aunque también existen implementaciones alternativas: Rubinius, JRuby, o adaptar la VM genérica Parrot:
http://patshaughnessy.net/2012/2/15/is-ruby-interpreted-or-compiled
https://en.wikipedia.org/wiki/Parrot_virtual_machine

Desensamblador para bytecode Ruby

La misma máquina virtual posee métodos para permitir presentar la secuencia de instrucciones correspondientes a determinado código fuente.
https://gist.github.com/rohit/1867939

bytecode= RubyVM::InstructionSequence.compile(code)
puts bytecode.disasm

Siendo “code” una cadena de caracteres que contenga el código fuente del programa o bloque de código que se quiere analizar.

Un programa “desensamblador” de código fuente Ruby sería:

ruby_disam.rb:

#!/usr/bin/ruby
# encoding: UTF-8

$cnt=0

ARGV.each do |cmd_arg|
  puts "#{$0}: ARGV[#{$cnt}]: #{cmd_arg}"
  $cnt=$cnt+1
end

$file_name=ARGV[0]

$code=""

File.open($file_name, "r").each_line do |line|
  $code = $code+line
end

puts "#{$0}: contents of file #{$file_name}...\n"
puts "..........................................."
puts $code
STDOUT.flush
puts "..........................................."

# Realizando desensamblado...
puts "#{$0}: saving VM bytecode..."

$bytecode = RubyVM::InstructionSequence.compile($code)

puts "#{$0}: doing VM bytecode disassembly:\n"
puts "..........................................."

puts $bytecode.disasm

puts "..........................................."
STDOUT.flush

La salida mostrando instrucciones de la Ruby VM:

$ ./ruby_disam.rb rb_loop.rb

...........................................

./ruby_disam.rb: saving VM bytecode...

./ruby_disam.rb: doing VM bytecode disassembly:

...........................................

==disasm: <RubyVM::InstructionSequence:@>==========

==catch table

| catch type: break st: 0019 ed: 0026 sp: 0000 cont: 0026

|------------------------------------------------------------------------

local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw:-1@-1, kwrest: -1])

[ 2] i 

0000 trace 1 ( 5)

0002 putobject_OP_INT2FIX_O_0_C_ 

0003 setglobal $i

0005 trace 1 ( 6)

0007 putobject 4

0009 setglobal $vect_len

0011 trace 1 ( 8)

0013 duparray [0.0]

0015 setglobal $vect_a

0017 trace 1 ( 14)

0019 putobject_OP_INT2FIX_O_1_C_ 

0020 getglobal $vect_len

0022 newrange 0

0024 send <callinfo!mid:each, argc:0, block:block in >

0026 leave 

==disasm: ===
 catch table

| catch
 type: redo st: 0006 ed: 0063 sp: 0000 cont: 0006

| catch
 type: next st: 0006 ed: 0063 sp: 0000 cont: 0063

|------------------------------------------------------------------------

local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])

[ 2] ? 

0000 getlocal_OP__WC__0 2 ( 18)

0002 setlocal_OP__WC__1 2 ( 14)

0004 trace 256

0006 trace 1 ( 15)

0008 getglobal $vect_a

0010 getglobal $i

0012 getinlinecache 21, 

0015 getconstant :Math

0017 getconstant :PI

0019 setinlinecache 

0021 getlocal_OP__WC__1 2

0023 opt_div 

0025 opt_aset 

0027 pop 

0028 trace 1 ( 16)

0030 putself 

0031 putobject "vect_a["

0033 getglobal $i

0035 tostring 

0036 putobject "]: "

0038 getglobal $vect_a

0040 getglobal $i

0042 opt_aref 

0044 tostring 

0045 putobject "\n"

0047 concatstrings 5

0049 opt_send_without_block 

0051 pop 

0052 trace 1 ( 17)

0054 getinlinecache 61, 

0057 getconstant :STDOUT

0059 setinlinecache 

0061 opt_send_without_block 

0063 trace 512 ( 18)

0065 leave ( 17)

Como ya se vió para Lua y Python, Ruby no presenta relación directa entre el lenguaje máquina de la plataforma de hardware y las líneas de código fuente de programa leídas.
Si existe relación con las instrucciones de la “máquina virtual” correspondiente. Luego, será la máquina virtual (ejecutada desde el intérprete Ruby) la que realmente realice las llamadas a sistema y ejecute “código máquina” para poder cumplir con la ejecución de su “bytecode”.

Para este caso, utilizando “strace”:

$ strace -e trace=write ruby ./rb_loop.rb 1>/dev/null 

write(1, "vect_a[0]: 3.141592653589793\n", 29) = 29

write(1, "vect_a[0]: 1.5707963267948966\n", 30) = 30

write(1, "vect_a[0]: 1.0471975511965976\n", 30) = 30

write(1, "vect_a[0]: 0.7853981633974483\n", 30) = 30

write(4, "!", 1) = 1

+++exited with 0 +++

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *