چگونه کد Node.js را با استفاده از Bytenode کامپایل کنیم؟ – راهنمای گام به گام


در این نوشته شیوه کامپایل واقعی کد Node.js یا همان جاوا اسکریپت به بایتکد V8 را با هم مرور میکنیم. بدین ترتیب میتوانید کد منبع خود را به روشی بهتر از «مبهم سازی» (obfuscation) یا ترفندهای نه چندان بهینه دیگر (مانند رمزنگاری کد با استفاده از یک کلید محرمانه که در فایلهای باینری اپلیکیشن گنجانده میشود) پنهان یا محافظت کنید. بنابراین با استفاده از ابزار Bytenode میتوانید یک نسخه باینری jsc. از فایلهای جاوا اسکریپت خود را توزیع کنید. همچنین میتوانید همه فایلهای js. را با استفاده از Browserify بستهبندی کنید و سپس به صورت یک فایل منفرد jsc. کامپایل نمایید.
به منظور کسب اطلاعات بیشتر میتوانید به ریپازیتوری Bytenode (+) روی گیتهاب مراجعه کنید.
خلاصه بحث
Bytenode را به صورت سراسری نصب کنید:
[sudo] npm install -g bytenode
برای کامپایل کردن فایل js. دستور زیر را اجرا کنید:
bytenode --compile my-file.js my-file.jsc
Bytenode را در پروژه خود نیز نصب کنید:
npm install --save bytenode
در کد خود bytenode را به صورت require بیاورید:
const bytenode = require('bytenode');
تا پسوند jsc. در سیستم ماژول Node.js ثبت شود. به همین دلیل است که آن را به صورت محلی نیز نصب کردیم.
اینک میتوانیم my-file.jsc را به صورت یک ماژول نیز require کنیم:
const myFile = require('./my-file.jsc');
همچنین میتوانید my-file.js را از build مربوط به production نیز حذف کنید و اگر بخواهید my-file.jsc را با استفاده از ابزار cli برای bytenode اجرا کنید، باید دستور زیر را وارد نمایید:
bytenode --run my-file.jsc
اکنون میدانیم که چگونه فایلهای js. را کامپایل کنیم، چگونه نسخه کامپایل شده را در کد خود require نماییم و چگونه فایلهای jsc. را از ترمینال اجرا کنیم. پس در ادامه به توضیح تفصیلی موارد فوق میپردازیم.
موتور V8 که Node.js بر مبنای آن طراحی شده است از چیزی به نام کامپایل کردن درجا (just in time compilation) یا به اختصار JIT استفاده میکند که در آن کد جاوا اسکریپت درست پیش از اجرا کامپایل میشود و سپس متعاقباً بهینهسازی صورت میگیرد.
از Node.js نسخه v5.7.0 در ماژول vm یک خصوصیت به نام produceCachedData در تابع سازنده vm.Script معرفی شده است. بنابراین اگر کدی به صورت زیر داشته باشید:
let helloScript = new vm.Script('console.log("Hello World!");', { produceCachedData: true /* This is required for Node.js < 10.0.0 */ });
در این صورت برای دستیابی به بایتکد یا بافر cachedData به صورت زیر عمل میکنیم:
let helloBuffer = helloScript.cachedData; // or in Node.js >= 10 let helloBuffer = helloScript.createCachedData();
از این helloBuffer میتوان برای ایجاد یک اسکریپت یکسان استفاده کرد که هنگام ارسال به تابع سازنده vm.Script همان دستورالعملها را در زمان اجرا ارائه میکند:
let anotherHelloScript = new vm.Script('', { produceCachedData: true, cachedData: helloBuffer }); // This will fail!
اما کد فوق موفق نخواهد بود و موتور V8 هنگام بررسی این که آیا این همان کدی است که برای تولید بافر helloBuffer در ابتدا استفاده شده است، در مورد آرگومان اول یعنی رشته خالی (‘’) هشدار میدهد. با این وجود این فرایند بررسی کاملاً آسان است و این طول کد است که مهم است. بنابراین کد زیر کار میکند:
let anotherHelloScript = new vm.Script(' '.repeat(28), { produceCachedData: true, cachedData: helloBuffer });
تنها کاری که کردیم این است که رشتهای خالی با همان طول (28) کد اصلی ;("!console.log("Hello World ایجاد کردیم.
این وضعیت جالب است زیرا با استفاده از بافر کش شده با همان طول کد اصلی میتوانیم یک اسکریپت یکسان ایجاد کنیم. هر دو اسکریپت را میتوان با استفاده از تابع ;()runInThisContext. اجرا کرد. بنابراین اگر دستورهای زیر را اجرا کنید:
helloScript.runInThisContext(); anotherHelloScript.runInThisContext();
عبارت !Hello World را دو بار مشاهده میکنید.
دقت کنید که اگر از طول نادرستی استفاده کرده باشید یا اگر از نسخه دیگری از Node.js/V8 استفاده کنید دیگر anotherHelloScript کار نمیکند و خصوصیت cachedDataRejected آن باید به صورت True تنظیم شود.
اکنون در گام آخر هنگام تعریف کردن anotherHelloScript از مقدار هارد کد شده (28) به عنوان طول کد خود استفاده کردهایم؛ اما چگونه میتوانیم دقیقاً بدانیم که کد اصلی چه طولی داشته است؟
پس از اندکی کاوش در کد منبع V8 درمییابیم که اطلاعات هدر این بخش در فایل code-serializer.h تعریف شدهاند:
// The data header consists of uint32_t-sized entries: // [0] magic number and (internally provided) external reference count // [1] version hash // [2] source hash // [3] cpu features // [4] flag hash
اما بافر Node.js آرایهای از نوع Uint8Array است. این بدان معنی است که هر مدخل از آرایه uint32، چهار مدخل در بافر uint8 میگیرد. بنابراین طول payload (که در source hash در اندیس [2] و در بافر Node بایتهای [8, 9, 10, 11] است) به صورت زیر خواهد بود:
let payloadLengthBytes = whateverBufferYouHave.slice(8, 12);
این مقدار چیزی مانند <Buffer 1c 00 00 00> خواهد بود که از نوع Little Endian است و از این رو به صورت 0x0000001c خوانده میشود. این همان طول کد ما یعنی عدد 28 در سیستم دهدهی است.
برای تبدیل این چهار بایت به مقدار عددی باید کاری مانند زیر را انجام دهید:
firstByte + (secodeByte * 256) + (thirdByte * 256**2) + (forthByte * 256**3),
روش بهتر این است که به صورت زیر عمل کنیم:
let length = payloadLengthBytes.reduce((sum, number, power) => sum += number * 256**power, 0);
به طور جایگزین میتوانیم از تابع ()buf.readIntLE استفاده کنیم که دقیقاً کاری که میخواهیم را انجام میدهد:
let length = whateverBufferYouHave.readIntLE(8, 4); // 8 is the offset, 4 is the number of bytes to read
زمانی که طول کد اصلی خوانده شد (از آن برای تولید بافر cachedData استفاده شده است) میتوانیم اسکریپت خود را بسازیم:
let anotherHelloScript = new vm.Script(' '.repeat(length), { produceCachedData: true, cachedData: helloBuffer }); // later in your code anotherHelloScript.runInThisContext();
سخن پایانی
در نهایت این پرسش مطرح میشود که آیا این تکنیک تأثیری روی عملکرد دارد یا نه؟ در نسخههای جدیدتر V8 (و Node.js)، عملکرد تقریباً یکسان مانده است. ما با استفاده از بنچمارک octance تغییری در عملکرد مشاهده نکردیم. با این که گوگل بنچمارک octance را به دلیل فریبکاری مرورگرها و موتورهای جاوا اسکریپت منسوخ اعلام کرده است؛ اما نتایج به دست آمده در مورد ما معنیدار هستند، زیرا ما کد یکسانی را روی موتور جاوا اسکریپت یکسانی مقایسه کردهایم. بنابراین پاسخ نهایی این است که Bytenode تأثیر منفی روی عملکرد ندارد.
مثالهای عملی کامل را میتوانید در این ریپازیتوری گیتهاب (+) مشاهده کنید. همچنین یک مثال از الکترون (+) اضافه شده است که هیچ حفاظت کد منبع ندارد و همچنین نمونهای برای NW.js (+) ارائه شده که ابزار مشابه nwjc را دارد؛ اما تنها در کدهای سمت کلاینت کار میکند.
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- Node.js چیست و چه نقشی در توسعه وب دارد؟ — به زبان ساده
- مجموعه آموزشهای دروس مهندسی کامپیوتر
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- Node.js و وب هوک های گیت هاب — راهنمای به روز رسانی پروژه ها از راه دور
- آموزش جاوا اسکریپت (JavaScript)
- سرور TCP مبتنی بر Node.js با استفاده از PM2 و Nginx — راهنمای ساخت اپلیکیشن از صفر تا صد
==