Android source code analysis – Binder mechanics from AIDL

Explore the principles of Binder mechanisms using AIDL as an entry point

Learn about Binder from AIDL

After a brief introduction to THE use of AIDL, Binder mechanics will be explored using AIDL. Binder Communication Architecture Binder Learning Guide Android Bander Design and Implementation – Design chapter

Background knowledge

The first thing to know is that in Linux, there are many processes, and data is not shared between different processes, they have their own space. Therefore, to exchange data between two processes, a mechanism is needed to make a data path.

Also, in Linux, there are two concepts: kernel space and user space. The Linux kernel is protected with a high degree of security, and our applications can only run in open user space. If an application needs to access kernel space, it needs to do so through a system call.

Therefore, to achieve communication between processes, we can create a “hub” in the kernel space. Although different processes cannot access each other’s memory, they can use system calls to access the “hub” we created. The Binder mechanism is such a “hub system”.

Since Android is based on Linux, why create your own Bidner mechanism instead of Linux? A good answer on Zhihu: Why Android uses Binder mechanism for IPC?

Binder communication model

First, in the kernel, there is something that acts as a hub called Binder drivers. With the Binder mechanism, each process ultimately needs to perform the handover of data here.

Second, there is a core service called a ServiceManager that, unlike Binder drivers, resides in user space.

These two are at the heart of the Binder mechanism.

For example, now there are two more processes, A and B, and A wants to access the method f() of an object obj in B. This is cross-process communication. Before doing so, process B registers itself with the ServiceManager, which means that there is a table and process B first inserts its own information into the table as a piece of data. After that, to access process B, process A simply needs to access the ServiceManager, look for B’s prior information, and then he can get the object obj, which can then call method f() directly.

In this process, you don’t actually get the object obj, just a proxy object of OBJ. The actual object is still in process B. When f() is called, the parameters passed in are passed only to the proxy object, which is then responsible for passing the data to the real object. In this whole process, A, B and ServiceManager are all processes, and the data exchange between them is directly delivered to the Binder driver. It’s a little confusing, but if you draw a picture, you can see it:

In the figure, the dotted line indicates that the two are not interacting directly, because all three are actually interacting through the solid line.

From the AIDL to the Binder

AIDL basically helped me implement a class that could be used as a Binder to communicate. We could write a similar class that could be used instead of AIDL. An inner class is defined through AIDl auto-generated classes that inherit from Binder classes. With Binder, we can do simple IPC.

Starting from the Client

Explore the flow of Binder communication, starting with MyAidlClient.

First, as shown in the figure

With the obtained IBinder object, the asInterface() method is called, which returns an object of the IMyAidlInterface class and calls its add() method. Here’s where to start:

IMyAidlInterface.java:

/** * Cast an IBinder object into an com.levent_j.myaidlserver.IMyAidlInterface interface, * generating a proxy if needed. */
        public static com.levent_j.myaidlserver.IMyAidlInterface asInterface(android.os.IBinder obj) {
            // Non-null judgment
            if ((obj == null)) {
                return null;
            }
            If yes, it can be used directly, and there is no need for cross-process communication
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if(((iin ! =null) && (iin instanceof com.levent_j.myaidlserver.IMyAidlInterface))) {
                return ((com.levent_j.myaidlserver.IMyAidlInterface) iin);
            }
            // If not found, we can only communicate across processes
            // A Proxy object is created from the IBinder object and returned
            // As the name indicates, a proxy object is returned
            return new com.levent_j.myaidlserver.IMyAidlInterface.Stub.Proxy(obj);
        }
Copy the code

Come again

// The first thing to know is that this class implements that interface
private static class Proxy implements com.levent_j.myaidlserver.IMyAidlInterface {
            // Holds a reference to an IBinder object
            private android.os.IBinder mRemote;
            // When the object is created, the proxy object only keeps a reference to the IBinder object
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            // Return the saved reference
            @Override
            public android.os.IBinder asBinder(a) {
                return mRemote;
            }
            / /.....................
        
        }
Copy the code

Calling the asInterface() method ends here, and you can see that it actually returns a Proxy object, so calling add() then calls the add() method of the Proxy object, which is the add() method of the Proxy class:


@Override
            public int add(int arg1, int agr2) throws android.os.RemoteException {
                // Create two Parcel objects
                The obtain() method means there is a cache pool of Parcel objects to avoid waste
                // A Parcel object is a data structure that supports cross-process objects
                // This _data is used to store the request parameters of the called method
                android.os.Parcel _data = android.os.Parcel.obtain();
                //_reply is used to store the returned results
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    // Write data to _data first
                    _data.writeInterfaceToken(DESCRIPTOR);
                    // Two arge are exactly the arguments we pass in
                    _data.writeInt(arg1);
                    _data.writeInt(agr2);
                    // The transact() method of the IBinder object is called
                    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    // Finally release
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
Copy the code

The IBinder class is an interface, and the objects retrieved here are actually objects of BinderProxy, the inner class in binder.java (what? The Proxy? That’s right, here’s another proxy). So once you get the BinderProxy object, as shown above, you call its transcat() method, pass in the parameters, pass the data to the Server, and get the return value after the add() of the actual object. So this is where Binder starts:

TRANSACTION_add = stub.transaction_add = stub.transaction_add = stub.transaction_add = stub.transaction_add
// Two Parcel objects, as data
// Finally flags is 0
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        // Check both parcels first
        Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");

        if (mWarnOnBlocking && ((flags & FLAG_ONEWAY) == 0)) {
            // For now, avoid spamming the log by disabling after we've logged
            // about this interface at least once
            mWarnOnBlocking = false;
            Log.w(Binder.TAG, "Outgoing transactions from this process must be FLAG_ONEWAY".new Throwable());
        }

        final boolean tracingEnabled = Binder.isTracingEnabled();
        if (tracingEnabled) {
            final Throwable tr = new Throwable();
            Binder.getTransactionTracker().addTrace(tr);
            StackTraceElement stackTraceElement = tr.getStackTrace()[1];
            Trace.traceBegin(Trace.TRACE_TAG_ALWAYS,
                    stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName());
        }
        try {
        // Finally, the transactNative() method is called, that is, the Native layer
            return transactNative(code, data, reply, flags);
        } finally {
            if(tracingEnabled) { Trace.traceEnd(Trace.TRACE_TAG_ALWAYS); }}}Copy the code

Look at the definition of this method:


public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;
            
Copy the code

From here, you enter the native layer:

In android_util_Binder. CPP in:

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj, jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    if (dataObj == NULL) {
        jniThrowNullPointerException(env, NULL);
        return JNI_FALSE;
    }
    // convert Java Parcel to C++ Parcel
    Parcel* data = parcelForJavaObject(env, dataObj);
    if (data == NULL) {
        return JNI_FALSE;
    }
    Parcel* reply = parcelForJavaObject(env, replyObj);
    if (reply == NULL&& replyObj ! =NULL) {
        return JNI_FALSE;
    }
    // Target points to BpBinder
    // This is when Zygote calls AndroidRuntime::startReg to register the JNI method
    // The register_android_os_Binder() procedure has an operation to initiate and register BinderProxy
    IBinder* target = (IBinder*)
        env->GetLongField(obj, gBinderProxyOffsets.mObject);
    if (target == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException"."Binder has been finalized!");
        return JNI_FALSE;
    }

    ALOGV("Java code calling transact on %p in Java object %p with code %" PRId32 "\n",
            target, obj, code);


    bool time_binder_calls;
    int64_t start_millis;
    if (kEnableBinderSample) {
        // Only log the binder call duration for things on the Java-level main thread.
        // But if we don't
        time_binder_calls = should_time_binder_calls(a);if (time_binder_calls) {
            start_millis = uptimeMillis();
        }
    }

    // Here is BpBinder transact()
    //printf("Transact from Java code to %p sending: ", target); data->print();
    status_t err = target->transact(code, *data, reply, flags);
    //if (reply) printf("Transact from Java code to %p received: ", target); reply->print();

    if (kEnableBinderSample) {
        if (time_binder_calls) {
            conditionally_log_binder_call(start_millis, target, code); }}if (err == NO_ERROR) {
        return JNI_TRUE;
    } else if (err == UNKNOWN_TRANSACTION) {
        return JNI_FALSE;
    }

    signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
    return JNI_FALSE;
}
Copy the code

Then there is the transact() method in bpBinder.cpp:

status_t BpBinder::transact( uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { // Once a binder has died, It will never come back to life. if (mAlive) {//IPCThreadState uses singleton mode // returns a status status_t status = IPCThreadState::self()->transact( mHandle, code, data, reply, flags); if (status == DEAD_OBJECT) mAlive = 0; return status; } return DEAD_OBJECT; }Copy the code

To the transact() method of ipcThreadState.cpp:

tatus_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { status_t err = data.errorCheck(); flags |= TF_ACCEPT_FDS; IF_LOG_TRANSACTIONS() { TextOutput::Bundle _b(alog); alog << "BC_TRANSACTION thr " << (void*)pthread_self() << " / hand " << handle << " / code " << TypeCode(code) << ": " << indent << data << dedent << endl; } if (err == NO_ERROR) { LOG_ONEWAY(">>>> SEND from pid %d uid %d %s", getpid(), getuid(), (flags & TF_ONE_WAY) == 0 ? "READ REPLY" : "ONE WAY"); Err = writeTransactionData(BC_TRANSACTION, FLAGS, Handle, code, data, NULL); } if (err ! = NO_ERROR) { if (reply) reply->setError(err); return (mLastError = err); If ((flags & TF_ONE_WAY) == 0) {#if 0 if (code == 4) {// relayout ALOGI(">>>>>> CALLING transaction 4"); } else { ALOGI(">>>>>> CALLING transaction %d", code); } #endif if (reply) {err = waitForResponse(reply); } else { Parcel fakeReply; err = waitForResponse(&fakeReply); } #if 0 if (code == 4) { // relayout ALOGI("<<<<<< RETURNING transaction 4"); } else { ALOGI("<<<<<< RETURNING transaction %d", code); } #endif IF_LOG_TRANSACTIONS() { TextOutput::Bundle _b(alog); alog << "BR_REPLY thr " << (void*)pthread_self() << " / hand " << handle << ": "; if (reply) alog << indent << *reply << dedent << endl; else alog << "(none requested)" << endl; } } else { err = waitForResponse(NULL, NULL); } return err; }Copy the code

The write method here is obviously writing data

Write data / / / / the CMD: BC_TRANSACTION status_t IPCThreadState: : writeTransactionData (int32_t CMD, uint32_t binderFlags, int32_t handle, uint32_t code, const Parcel& data, Status_t * statusBuffer) {// Create a binder_transaction_data data structure binder_transaction_data tr; tr.target.ptr = 0; /* Don't pass uninitialized stack data to a remote process */ tr.target.handle = handle; //handle refers to AMS tr.code = code; tr.flags = binderFlags; tr.cookie = 0; tr.sender_pid = 0; tr.sender_euid = 0; const status_t err = data.errorCheck(); If (err == NO_ERROR) {tr.data_size = data.ipcdatasize (); // If (err == NO_ERROR) {tr.data_size = data.ipcdatasize (); tr.data.ptr.buffer = data.ipcData(); tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t); tr.data.ptr.offsets = data.ipcObjects(); } else if (statusBuffer) { tr.flags |= TF_STATUS_CODE; *statusBuffer = err; tr.data_size = sizeof(status_t); tr.data.ptr.buffer = reinterpret_cast<uintptr_t>(statusBuffer); tr.offsets_size = 0; tr.data.ptr.offsets = 0; } else { return (mLastError = err); } // Write data to mout.writeint32 (CMD); mOut.write(&tr, sizeof(tr)); return NO_ERROR; }Copy the code

So you can see that mOut is used to write the data so after writing the data, you go to this waitForResponse() method, waiting for the response

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult) { uint32_t cmd; int32_t err; While (1) {// Call taklWithDriver(), Return an error code // break if ((err=talkWithDriver()) < NO_ERROR) break; err = mIn.errorCheck(); if (err < NO_ERROR) break; If (min.dataAvail () == 0) continue; // If (min.dataAvail () == 0) continue; CMD = (uint32_t) min.readint32 (); IF_LOG_COMMANDS() { alog << "Processing waitForResponse Command: " << getReturnString(cmd) << endl; } // Through CMD switch (CMD) {// This communication end case BR_TRANSACTION_COMPLETE: if (! reply && ! acquireResult) goto finish; break; case BR_DEAD_REPLY: err = DEAD_OBJECT; goto finish; case BR_FAILED_REPLY: err = FAILED_TRANSACTION; goto finish; case BR_ACQUIRE_RESULT: { ALOG_ASSERT(acquireResult ! = NULL, "Unexpected brACQUIRE_RESULT"); const int32_t result = mIn.readInt32(); if (! acquireResult) continue; *acquireResult = result ? NO_ERROR : INVALID_OPERATION; } goto finish; case BR_REPLY: { binder_transaction_data tr; err = mIn.read(&tr, sizeof(tr)); ALOG_ASSERT(err == NO_ERROR, "Not enough command data for brREPLY"); if (err ! = NO_ERROR) goto finish; if (reply) { if ((tr.flags & TF_STATUS_CODE) == 0) { reply->ipcSetDataReference( reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer), tr.data_size, reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets), tr.offsets_size/sizeof(binder_size_t), freeBuffer, this); } else { err = *reinterpret_cast<const status_t*>(tr.data.ptr.buffer); freeBuffer(NULL, reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer), tr.data_size, reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets), tr.offsets_size/sizeof(binder_size_t), this); } } else { freeBuffer(NULL, reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer), tr.data_size, reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets), tr.offsets_size/sizeof(binder_size_t), this); continue; } } goto finish; default: err = executeCommand(cmd); if (err ! = NO_ERROR) goto finish; break; } } finish: if (err ! = NO_ERROR) { if (acquireResult) *acquireResult = err; if (reply) reply->setError(err); mLastError = err; } return err; }Copy the code

There is also the talkWithDriver() method:

MOut already has data, mIn does not. Here to process the data status_t IPCThreadState: : talkWithDriver (bool doReceive) {if (mProcess - > mDriverFD < = 0) {return - EBADF; } binder_write_read bwr; // Is the read buffer empty? const bool needRead = mIn.dataPosition() >= mIn.dataSize(); // We don't want to write anything if we are still reading // from data left in the input buffer and the caller // has requested to read the next data. const size_t outAvail = (! doReceive || needRead) ? mOut.dataSize() : 0; bwr.write_size = outAvail; bwr.write_buffer = (uintptr_t)mOut.data(); // This is what we'll read. if (doReceive && needRead) { bwr.read_size = mIn.dataCapacity(); bwr.read_buffer = (uintptr_t)mIn.data(); } else { bwr.read_size = 0; bwr.read_buffer = 0; } IF_LOG_COMMANDS() { TextOutput::Bundle _b(alog); if (outAvail ! = 0) { alog << "Sending commands to driver: " << indent; const void* cmds = (const void*)bwr.write_buffer; const void* end = ((const uint8_t*)cmds)+bwr.write_size; alog << HexDump(cmds, bwr.write_size) << endl; while (cmds < end) cmds = printCommand(alog, cmds); alog << dedent; } alog << "Size of receive buffer: " << bwr.read_size << ", needRead: " << needRead << ", doReceive: " << doReceive << endl; // Return immediately if there is nothing to do. if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR; bwr.write_consumed = 0; bwr.read_consumed = 0; status_t err; do { IF_LOG_COMMANDS() { alog << "About to read/write, write size = " << mOut.dataSize() << endl; } #if defined(__ANDROID__) //ioctl() is called by Binder Driver, which is used to communicate with the Driver. BINDER_WRITE_READ, &bwr) >= 0) err = NO_ERROR; else err = -errno; #else err = INVALID_OPERATION; #endif if (mProcess->mDriverFD <= 0) { err = -EBADF; } IF_LOG_COMMANDS() { alog << "Finished read/write, write size = " << mOut.dataSize() << endl; } } while (err == -EINTR); IF_LOG_COMMANDS() { alog << "Our err: " << (void*)(intptr_t)err << ", write consumed: " << bwr.write_consumed << " (of " << mOut.dataSize() << "), read consumed: " << bwr.read_consumed << endl; } if (err >= NO_ERROR) { if (bwr.write_consumed > 0) { if (bwr.write_consumed < mOut.dataSize()) mOut.remove(0, bwr.write_consumed); else mOut.setDataSize(0); } if (bwr.read_consumed > 0) { mIn.setDataSize(bwr.read_consumed); mIn.setDataPosition(0); } IF_LOG_COMMANDS() { TextOutput::Bundle _b(alog); alog << "Remaining data size: " << mOut.dataSize() << endl; alog << "Received commands from driver: " << indent; const void* cmds = mIn.data(); const void* end = mIn.data() + mIn.dataSize(); alog << HexDump(cmds, mIn.dataSize()) << endl; while (cmds < end) cmds = printReturnCommand(alog, cmds); alog << dedent; } return NO_ERROR; } return err; }Copy the code

So the talkWithDriver() method communicates directly with the Binder driver, whose core is the ioctl() method.

At the end of the communication, go back to the original starting point, wait for the return value (if any), and finally get the returned data from mIn.

What does the Server do

As you know, the data goes through the Binder agent and is now on the Server side. The main processing is in the onTransact() method of the inner Stub:

@Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                // It is this method identifier that calls the add() method
                case TRANSACTION_add: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    // Call add()
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true; }}return super.onTransact(code, data, reply, flags);
        }
Copy the code

Remember, the add() method was rewritten on the Server side, and yes, that’s the call to the add() method

conclusion

In general, the Binder process is:

  1. Client
  2. Create binder_transaction_data
  3. Fill in the code
  4. Fill the parameter with data.buffer
  5. Fill in target.handle(Client reference)
  6. BC_TRANSACTION is sent to the Binder driver
  7. Find the target and fill in target.ptr(Server side entity)
  8. To the receiving thread
  9. Call the onTransact() method
  10. Server

We say that one advantage of Bidner over other IPC mechanisms is that there is only one copy. So what’s going on here? The binder_transaction_data can be divided into several parts when data is copied, but only one part, called buffer, is of unpredictable size. The rest of the data is actually limited in size. The buffer is provided by the recipient itself, and the buffer is provided by the buffer pool. This completes the “one copy” of the data, giving the impression that the complete data is copied directly from the Client to the Server, actually using the kernel.